2022-03-22 14:44:44 +01:00
|
|
|
// @flow
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
import classnames from 'classnames';
|
|
|
|
import Icon from 'component/common/icon';
|
|
|
|
import * as ICONS from 'constants/icons';
|
|
|
|
import 'scss/component/homepage-sort.scss';
|
|
|
|
|
|
|
|
// prettier-ignore
|
|
|
|
const Lazy = {
|
|
|
|
// $FlowFixMe: cannot resolve dnd
|
|
|
|
DragDropContext: React.lazy(() => import('react-beautiful-dnd' /* webpackChunkName: "dnd" */).then((module) => ({ default: module.DragDropContext }))),
|
|
|
|
// $FlowFixMe: cannot resolve dnd
|
|
|
|
Droppable: React.lazy(() => import('react-beautiful-dnd' /* webpackChunkName: "dnd" */).then((module) => ({ default: module.Droppable }))),
|
|
|
|
// $FlowFixMe: cannot resolve dnd
|
|
|
|
Draggable: React.lazy(() => import('react-beautiful-dnd' /* webpackChunkName: "dnd" */).then((module) => ({ default: module.Draggable }))),
|
|
|
|
};
|
|
|
|
|
|
|
|
const NON_CATEGORY = Object.freeze({
|
|
|
|
FOLLOWING: { label: 'Following' },
|
|
|
|
FYP: { label: 'Recommended' },
|
|
|
|
});
|
|
|
|
|
|
|
|
// ****************************************************************************
|
|
|
|
// Helpers
|
|
|
|
// ****************************************************************************
|
|
|
|
|
|
|
|
const reorder = (list, startIndex, endIndex) => {
|
|
|
|
const result = Array.from(list);
|
|
|
|
const [removed] = result.splice(startIndex, 1);
|
|
|
|
result.splice(endIndex, 0, removed);
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
const move = (source, destination, droppableSource, droppableDestination) => {
|
|
|
|
const sourceClone = Array.from(source);
|
|
|
|
const destClone = Array.from(destination);
|
|
|
|
const [removed] = sourceClone.splice(droppableSource.index, 1);
|
|
|
|
destClone.splice(droppableDestination.index, 0, removed);
|
|
|
|
|
|
|
|
return {
|
|
|
|
[droppableSource.droppableId]: sourceClone,
|
|
|
|
[droppableDestination.droppableId]: destClone,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
function getInitialList(listId, savedOrder, homepageSections) {
|
|
|
|
const savedActiveOrder = savedOrder.active || [];
|
|
|
|
const savedHiddenOrder = savedOrder.hidden || [];
|
|
|
|
const sectionKeys = Object.keys(homepageSections);
|
|
|
|
|
2022-03-26 20:42:41 +01:00
|
|
|
if (sectionKeys.includes('NEWS') && !savedHiddenOrder.includes('NEWS') && !savedActiveOrder.includes('NEWS')) {
|
|
|
|
savedHiddenOrder.push('NEWS');
|
|
|
|
}
|
|
|
|
|
2022-03-22 14:44:44 +01:00
|
|
|
if (listId === 'ACTIVE') {
|
|
|
|
// Start with saved order, excluding obsolete items (i.e. category removed or not available in non-English)
|
|
|
|
const finalOrder = savedActiveOrder.filter((x) => sectionKeys.includes(x));
|
|
|
|
|
|
|
|
// Add new items (e.g. new categories)
|
|
|
|
sectionKeys.forEach((x) => {
|
|
|
|
if (!finalOrder.includes(x)) {
|
|
|
|
finalOrder.push(x);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Exclude items that were moved to Hidden
|
|
|
|
return finalOrder.filter((x) => !savedHiddenOrder.includes(x));
|
|
|
|
} else {
|
|
|
|
console.assert(listId === 'HIDDEN', `Unhandled listId: ${listId}`);
|
|
|
|
return savedHiddenOrder.filter((x) => sectionKeys.includes(x));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ****************************************************************************
|
|
|
|
// HomepageSort
|
|
|
|
// ****************************************************************************
|
|
|
|
|
|
|
|
type HomepageOrder = { active: ?Array<string>, hidden: ?Array<string> };
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
onUpdate: (newOrder: HomepageOrder) => void,
|
|
|
|
// --- redux:
|
|
|
|
homepageData: any,
|
|
|
|
homepageOrder: HomepageOrder,
|
|
|
|
};
|
|
|
|
|
|
|
|
export default function HomepageSort(props: Props) {
|
|
|
|
const { onUpdate, homepageData, homepageOrder } = props;
|
|
|
|
|
|
|
|
const SECTIONS = { ...NON_CATEGORY, ...homepageData };
|
|
|
|
const [listActive, setListActive] = useState(() => getInitialList('ACTIVE', homepageOrder, SECTIONS));
|
|
|
|
const [listHidden, setListHidden] = useState(() => getInitialList('HIDDEN', homepageOrder, SECTIONS));
|
|
|
|
|
|
|
|
const BINS = {
|
|
|
|
ACTIVE: { id: 'ACTIVE', title: 'Active', list: listActive, setList: setListActive },
|
|
|
|
HIDDEN: { id: 'HIDDEN', title: 'Hidden', list: listHidden, setList: setListHidden },
|
|
|
|
};
|
|
|
|
|
|
|
|
function onDragEnd(result) {
|
|
|
|
const { source, destination } = result;
|
|
|
|
|
|
|
|
if (destination) {
|
|
|
|
if (source.droppableId === destination.droppableId) {
|
|
|
|
const newList = reorder(BINS[source.droppableId].list, source.index, destination.index);
|
|
|
|
BINS[source.droppableId].setList(newList);
|
|
|
|
} else {
|
|
|
|
const result = move(BINS[source.droppableId].list, BINS[destination.droppableId].list, source, destination);
|
|
|
|
BINS[source.droppableId].setList(result[source.droppableId]);
|
|
|
|
BINS[destination.droppableId].setList(result[destination.droppableId]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const draggedItemRef = React.useRef();
|
|
|
|
|
|
|
|
const DraggableItem = ({ item, index }: any) => {
|
|
|
|
return (
|
|
|
|
<Lazy.Draggable draggableId={item} index={index}>
|
|
|
|
{(draggableProvided, snapshot) => {
|
|
|
|
if (snapshot.isDragging) {
|
|
|
|
// https://github.com/atlassian/react-beautiful-dnd/issues/1881#issuecomment-691237307
|
|
|
|
// $FlowFixMe
|
|
|
|
draggableProvided.draggableProps.style.left = draggedItemRef.offsetLeft;
|
|
|
|
// $FlowFixMe
|
|
|
|
draggableProvided.draggableProps.style.top = draggedItemRef.offsetTop;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className="homepage-sort__entry"
|
|
|
|
ref={draggableProvided.innerRef}
|
|
|
|
{...draggableProvided.draggableProps}
|
|
|
|
{...draggableProvided.dragHandleProps}
|
|
|
|
>
|
|
|
|
<div ref={draggedItemRef}>
|
|
|
|
<Icon icon={ICONS.MENU} title={__('Drag')} size={20} />
|
|
|
|
</div>
|
|
|
|
{__(SECTIONS[item].label)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
</Lazy.Draggable>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const DroppableBin = ({ bin, className }: any) => {
|
|
|
|
return (
|
|
|
|
<Lazy.Droppable droppableId={bin.id}>
|
|
|
|
{(provided, snapshot) => (
|
|
|
|
<div
|
|
|
|
ref={provided.innerRef}
|
|
|
|
{...provided.droppableProps}
|
|
|
|
className={classnames('homepage-sort__bin', className, {
|
|
|
|
'homepage-sort__bin--highlight': snapshot.isDraggingOver,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<div className="homepage-sort__bin-header">{__(bin.title)}</div>
|
|
|
|
{bin.list.map((item, index) => (
|
|
|
|
<DraggableItem key={item} item={item} index={index} />
|
|
|
|
))}
|
|
|
|
{provided.placeholder}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</Lazy.Droppable>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (onUpdate) {
|
|
|
|
return onUpdate({ active: listActive, hidden: listHidden });
|
|
|
|
}
|
|
|
|
}, [listActive, listHidden, onUpdate]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<React.Suspense fallback={null}>
|
|
|
|
<div className="homepage-sort">
|
|
|
|
<Lazy.DragDropContext onDragEnd={onDragEnd}>
|
|
|
|
<DroppableBin bin={BINS.ACTIVE} />
|
|
|
|
<DroppableBin bin={BINS.HIDDEN} className="homepage-sort__bin--no-bg homepage-sort__bin--dashed" />
|
|
|
|
</Lazy.DragDropContext>
|
|
|
|
</div>
|
|
|
|
</React.Suspense>
|
|
|
|
);
|
|
|
|
}
|