From 8d46bcfd9affd1ccf6c80b46541852a19a1af1a3 Mon Sep 17 00:00:00 2001 From: siddharthkp Date: Fri, 4 Sep 2020 12:19:58 +0200 Subject: [PATCH 1/2] reorder things in profile --- .../overmind/namespaces/profile/actions.ts | 45 ++++++ .../app/overmind/namespaces/profile/state.ts | 4 +- .../src/app/pages/Profile2/AllSandboxes.tsx | 11 +- .../app/pages/Profile2/PinnedSandboxes.tsx | 15 +- .../src/app/pages/Profile2/SandboxCard.tsx | 139 ++++++++++++++++-- .../app/src/app/pages/Profile2/constants.ts | 6 + packages/app/src/app/pages/Profile2/index.tsx | 2 + 7 files changed, 203 insertions(+), 19 deletions(-) diff --git a/packages/app/src/app/overmind/namespaces/profile/actions.ts b/packages/app/src/app/overmind/namespaces/profile/actions.ts index 4301ea077d3..635875fe574 100755 --- a/packages/app/src/app/overmind/namespaces/profile/actions.ts +++ b/packages/app/src/app/overmind/namespaces/profile/actions.ts @@ -243,6 +243,9 @@ export const addFeaturedSandboxes: AsyncAction<{ sandbox => sandbox.id ); + // already featured + if (currentFeaturedSandboxIds.includes(sandboxId)) return; + // optimistic update actions.profile.addFeaturedSandboxesInState({ sandboxId }); @@ -294,6 +297,48 @@ export const removeFeaturedSandboxes: AsyncAction<{ } }; +export const reorderFeaturedSandboxesInState: Action<{ + startPosition: number; + endPosition: number; +}> = ({ state, actions, effects }, { startPosition, endPosition }) => { + if (!state.profile.current) return; + + // optimisic update + const featuredSandboxes = [...state.profile.current.featuredSandboxes]; + const sandbox = featuredSandboxes.find((_, index) => index === startPosition); + + // remove element first + featuredSandboxes.splice(startPosition, 1); + // now add at new position + featuredSandboxes.splice(endPosition, 0, sandbox); + + state.profile.current.featuredSandboxes = featuredSandboxes; +}; + +export const saveFeaturedSandboxesOrder: AsyncAction = async ({ + actions, + effects, + state, +}) => { + try { + const featuredSandboxIds = state.profile.current.featuredSandboxes.map( + s => s.id + ); + const profile = await effects.api.updateUserFeaturedSandboxes( + state.profile.current.id, + featuredSandboxIds + ); + state.profile.current.featuredSandboxes = profile.featuredSandboxes; + } catch (error) { + // TODO: rollback optimisic update + + actions.internal.handleError({ + message: "We weren't able to re-order your pinned sandboxes", + error, + }); + } +}; + export const changeSandboxPrivacyInState: Action<{ sandboxId: string; privacy: 0 | 1 | 2; diff --git a/packages/app/src/app/overmind/namespaces/profile/state.ts b/packages/app/src/app/overmind/namespaces/profile/state.ts index b1b3f58608d..b334e0060e8 100755 --- a/packages/app/src/app/overmind/namespaces/profile/state.ts +++ b/packages/app/src/app/overmind/namespaces/profile/state.ts @@ -50,6 +50,8 @@ export const state: State = { searchQuery: null, isLoadingSandboxes: false, sandboxToDeleteId: null, + currentSortBy: 'view_count', + currentSortDirection: 'desc', isProfileCurrentUser: derived((currentState: State, rootState: RootState) => Boolean( rootState.user && rootState.user.id === currentState.currentProfileId @@ -75,6 +77,4 @@ export const state: State = { ? currentState.sandboxes[currentState.current.username] : [] ), - currentSortBy: 'view_count', - currentSortDirection: 'desc', }; diff --git a/packages/app/src/app/pages/Profile2/AllSandboxes.tsx b/packages/app/src/app/pages/Profile2/AllSandboxes.tsx index 026fa95aac3..58e25006913 100644 --- a/packages/app/src/app/pages/Profile2/AllSandboxes.tsx +++ b/packages/app/src/app/pages/Profile2/AllSandboxes.tsx @@ -8,9 +8,10 @@ import { Menu, } from '@codesandbox/components'; import css from '@styled-system/css'; +import { motion } from 'framer-motion'; import { useOvermind } from 'app/overmind'; import { SandboxCard, SkeletonCard } from './SandboxCard'; -import { SANDBOXES_PER_PAGE } from './constants'; +import { SANDBOXES_PER_PAGE, SandboxTypes } from './constants'; export const AllSandboxes = ({ menuControls }) => { const { @@ -119,7 +120,13 @@ export const AllSandboxes = ({ menuControls }) => { )) : sandboxes.map((sandbox, index) => ( - + + + ))} diff --git a/packages/app/src/app/pages/Profile2/PinnedSandboxes.tsx b/packages/app/src/app/pages/Profile2/PinnedSandboxes.tsx index 314e60e167c..8a8818cccf6 100644 --- a/packages/app/src/app/pages/Profile2/PinnedSandboxes.tsx +++ b/packages/app/src/app/pages/Profile2/PinnedSandboxes.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { useOvermind } from 'app/overmind'; import { useDrop } from 'react-dnd'; +import { motion } from 'framer-motion'; import { Grid, Column, Stack, Text } from '@codesandbox/components'; import css from '@styled-system/css'; import { SandboxCard } from './SandboxCard'; +import { SandboxTypes } from './constants'; export const PinnedSandboxes = ({ menuControls }) => { const { @@ -16,7 +18,7 @@ export const PinnedSandboxes = ({ menuControls }) => { const myProfile = loggedInUser?.username === user.username; const [{ isOver }, drop] = useDrop({ - accept: 'sandbox', + accept: [SandboxTypes.ALL_SANDBOX, SandboxTypes.PINNED_SANDBOX], drop: () => ({ name: 'PINNED_SANDBOXES' }), collect: monitor => ({ isOver: monitor.isOver(), @@ -31,9 +33,16 @@ export const PinnedSandboxes = ({ menuControls }) => { gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', }} > - {user.featuredSandboxes.map(sandbox => ( + {user.featuredSandboxes.map((sandbox, index) => ( - + + + ))} {myProfile && ( diff --git a/packages/app/src/app/pages/Profile2/SandboxCard.tsx b/packages/app/src/app/pages/Profile2/SandboxCard.tsx index 6189dac8baf..4aaa5bdc213 100644 --- a/packages/app/src/app/pages/Profile2/SandboxCard.tsx +++ b/packages/app/src/app/pages/Profile2/SandboxCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useDrag } from 'react-dnd'; +import { useDrag, useDrop } from 'react-dnd'; import { useOvermind } from 'app/overmind'; import { Stack, @@ -11,36 +11,148 @@ import { } from '@codesandbox/components'; import css from '@styled-system/css'; import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator'; +import { SandboxTypes } from './constants'; + +type DragItem = { type: 'sandbox'; sandboxId: string; index: number | null }; export const SandboxCard = ({ + type = SandboxTypes.DEFAULT_SANDBOX, sandbox, menuControls: { onKeyDown, onContextMenu }, + index = null, }) => { const { state: { user: loggedInUser, - profile: { current: user }, + profile: { + current: { username, featuredSandboxes }, + }, }, actions: { - profile: { addFeaturedSandboxes }, + profile: { + addFeaturedSandboxesInState, + addFeaturedSandboxes, + reorderFeaturedSandboxesInState, + saveFeaturedSandboxesOrder, + removeFeaturedSandboxesInState, + }, }, } = useOvermind(); - const [, drag] = useDrag({ - item: { id: sandbox.id, type: 'sandbox' }, - end: (item: { id: string }, monitor) => { + const ref = React.useRef(null); + let previousPosition: number; + + const [{ isDragging }, drag] = useDrag({ + item: { type, sandboxId: sandbox.id, index }, + collect: monitor => { + const dragItem = monitor.getItem(); + return { + isDragging: dragItem?.sandboxId === sandbox.id, + }; + }, + + begin: () => { + if (type === SandboxTypes.PINNED_SANDBOX) { + previousPosition = index; + } + }, + end: (item: DragItem, monitor) => { const dropResult = monitor.getDropResult(); + + if (!dropResult) { + // This is the cancel event + if (item.type === SandboxTypes.PINNED_SANDBOX) { + // Rollback any reordering + reorderFeaturedSandboxesInState({ + startPosition: index, + endPosition: previousPosition, + }); + } else { + // remove newly added from featured in state + removeFeaturedSandboxesInState({ sandboxId: item.sandboxId }); + } + + return; + } + if (dropResult.name === 'PINNED_SANDBOXES') { - const { id } = item; - addFeaturedSandboxes({ sandboxId: id }); + if (featuredSandboxes.find(s => s.id === item.sandboxId)) { + saveFeaturedSandboxesOrder(); + } else { + addFeaturedSandboxes({ sandboxId: item.sandboxId }); + } + } + }, + }); + + const [, drop] = useDrop({ + accept: [SandboxTypes.ALL_SANDBOX, SandboxTypes.PINNED_SANDBOX], + hover: (item: DragItem, monitor) => { + if (!ref.current) return; + + const hoverIndex = index; + let dragIndex = -1; // not in list + + if (item.type === SandboxTypes.PINNED_SANDBOX) { + dragIndex = item.index; + } + + if (item.type === SandboxTypes.ALL_SANDBOX) { + // When an item from ALL_SANDOXES is hoverered over + // an item in pinned sandboxes, we insert the sandbox + // into featuredSandboxes in state. + if ( + hoverIndex && + !featuredSandboxes.find(s => s.id === item.sandboxId) + ) { + addFeaturedSandboxesInState({ sandboxId: item.sandboxId }); + } + dragIndex = featuredSandboxes.findIndex(s => s.id === item.sandboxId); } + + // If the item doesn't exist in featured sandboxes yet, return + if (dragIndex === -1) return; + + // Don't replace items with themselves + if (dragIndex === hoverIndex) return; + + // Determine rectangle for hoverered item + const hoverBoundingRect = ref.current?.getBoundingClientRect(); + + // Get offsets for dragged item + const dragOffset = monitor.getClientOffset(); + const hoverClientX = dragOffset.x - hoverBoundingRect.left; + + const hoverMiddleX = + (hoverBoundingRect.right - hoverBoundingRect.left) / 2; + + // Only perform the move when the mouse has crossed half of the items width + + // Dragging forward + if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX) return; + + // Dragging backward + if (dragIndex > hoverIndex && hoverClientX > hoverMiddleX) return; + + reorderFeaturedSandboxesInState({ + startPosition: dragIndex, + endPosition: hoverIndex, + }); + // We're mutating the monitor item here to avoid expensive index searches! + item.index = hoverIndex; }, + drop: () => ({ name: 'PINNED_SANDBOXES' }), }); - const myProfile = loggedInUser?.username === user.username; + const myProfile = loggedInUser?.username === username; + + if (myProfile) { + if (type === SandboxTypes.ALL_SANDBOX) drag(ref); + else if (type === SandboxTypes.PINNED_SANDBOX) drag(drop(ref)); + } return ( -
+
onContextMenu(event, sandbox.id)} onKeyDown={event => onKeyDown(event, sandbox.id)} + style={{ opacity: isDragging ? 0.2 : 1 }} css={css({ backgroundColor: 'grays.700', border: '1px solid', @@ -82,8 +195,10 @@ export const SandboxCard = ({ }} /> - - {sandbox.title || sandbox.alias || sandbox.id} + + + {sandbox.title || sandbox.alias || sandbox.id} + Date: Fri, 4 Sep 2020 15:48:14 +0200 Subject: [PATCH 2/2] thank you typescript --- packages/app/src/app/overmind/namespaces/profile/actions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/app/overmind/namespaces/profile/actions.ts b/packages/app/src/app/overmind/namespaces/profile/actions.ts index 635875fe574..e19b9a8bc6a 100755 --- a/packages/app/src/app/overmind/namespaces/profile/actions.ts +++ b/packages/app/src/app/overmind/namespaces/profile/actions.ts @@ -305,7 +305,7 @@ export const reorderFeaturedSandboxesInState: Action<{ // optimisic update const featuredSandboxes = [...state.profile.current.featuredSandboxes]; - const sandbox = featuredSandboxes.find((_, index) => index === startPosition); + const sandbox = featuredSandboxes[startPosition]!; // remove element first featuredSandboxes.splice(startPosition, 1); @@ -320,6 +320,8 @@ export const saveFeaturedSandboxesOrder: AsyncAction = async ({ effects, state, }) => { + if (!state.profile.current) return; + try { const featuredSandboxIds = state.profile.current.featuredSandboxes.map( s => s.id