lbry-desktop/ui/component/homepageSort/view.jsx
infinite-persistence 5b16b3b058 Ability to customize homepage
Ticket: 1145

- Limited to premium for now (early access).
- Needed to handle custom categories from non-English homepages.
2022-03-23 15:35:27 -04:00

179 lines
6.3 KiB
JavaScript

// @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);
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>
);
}