Ability to customize homepage
Ticket: 1145 - Limited to premium for now (early access). - Needed to handle custom categories from non-English homepages.
This commit is contained in:
parent
d854a99250
commit
5b16b3b058
16 changed files with 495 additions and 106 deletions
|
@ -2147,7 +2147,7 @@
|
||||||
"You will not see scheduled livestreams by people you follow on the home or following page.": "You will not see scheduled livestreams by people you follow on the home or following page.",
|
"You will not see scheduled livestreams by people you follow on the home or following page.": "You will not see scheduled livestreams by people you follow on the home or following page.",
|
||||||
"Scheduled streams hidden, you can re-enable them in settings.": "Scheduled streams hidden, you can re-enable them in settings.",
|
"Scheduled streams hidden, you can re-enable them in settings.": "Scheduled streams hidden, you can re-enable them in settings.",
|
||||||
"Hide Personal Recommendations": "Hide Personal Recommendations",
|
"Hide Personal Recommendations": "Hide Personal Recommendations",
|
||||||
"Recommendations hidden; you can re-enable them in Settings.": "Recommendations hidden; you can re-enable them in Settings.",
|
"Clean as a whistle! --[title for empty homepage]--": "Clean as a whistle!",
|
||||||
"You will not see the personal recommendations in the homepage.": "You will not see the personal recommendations in the homepage.",
|
"You will not see the personal recommendations in the homepage.": "You will not see the personal recommendations in the homepage.",
|
||||||
"Sorry, the service is currently unavailable.": "Sorry, the service is currently unavailable.",
|
"Sorry, the service is currently unavailable.": "Sorry, the service is currently unavailable.",
|
||||||
"Started %time_date%": "Started %time_date%",
|
"Started %time_date%": "Started %time_date%",
|
||||||
|
|
13
ui/component/homepageSort/index.js
Normal file
13
ui/component/homepageSort/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import HomepageSort from './view';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { selectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
homepageData: selectHomepageData(state),
|
||||||
|
homepageOrder: selectClientSetting(state, SETTINGS.HOMEPAGE_ORDER),
|
||||||
|
});
|
||||||
|
|
||||||
|
// const perform = (dispatch) => ({});
|
||||||
|
|
||||||
|
export default connect(select)(HomepageSort);
|
179
ui/component/homepageSort/view.jsx
Normal file
179
ui/component/homepageSort/view.jsx
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
// @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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,10 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
import { doFetchPersonalRecommendations } from 'redux/actions/search';
|
import { doFetchPersonalRecommendations } from 'redux/actions/search';
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
|
||||||
import { selectPersonalRecommendations } from 'redux/selectors/search';
|
import { selectPersonalRecommendations } from 'redux/selectors/search';
|
||||||
import { selectClientSetting } from 'redux/selectors/settings';
|
|
||||||
import { selectHasOdyseeMembership, selectUser } from 'redux/selectors/user';
|
import { selectHasOdyseeMembership, selectUser } from 'redux/selectors/user';
|
||||||
|
|
||||||
import RecommendedPersonal from './view';
|
import RecommendedPersonal from './view';
|
||||||
|
@ -15,14 +11,11 @@ const select = (state) => {
|
||||||
userId: user && user.id,
|
userId: user && user.id,
|
||||||
personalRecommendations: selectPersonalRecommendations(state),
|
personalRecommendations: selectPersonalRecommendations(state),
|
||||||
hasMembership: selectHasOdyseeMembership(state),
|
hasMembership: selectHasOdyseeMembership(state),
|
||||||
hideFyp: selectClientSetting(state, SETTINGS.HIDE_FYP),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = {
|
const perform = {
|
||||||
doFetchPersonalRecommendations,
|
doFetchPersonalRecommendations,
|
||||||
doSetClientSetting,
|
|
||||||
doToast,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(select, perform)(RecommendedPersonal);
|
export default connect(select, perform)(RecommendedPersonal);
|
||||||
|
|
|
@ -1,44 +1,16 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import HelpLink from 'component/common/help-link';
|
|
||||||
import Icon from 'component/common/icon';
|
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
import { URL, SHARE_DOMAIN_URL } from 'config';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import { useIsLargeScreen, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsLargeScreen, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
|
|
||||||
// TODO: recsysFyp will be moved into 'RecSys', so the redux import in a jsx
|
// TODO: recsysFyp will be moved into 'RecSys', so the redux import in a jsx
|
||||||
// violation is just temporary.
|
// violation is just temporary.
|
||||||
import { recsysFyp } from 'redux/actions/search';
|
import { recsysFyp } from 'redux/actions/search';
|
||||||
|
|
||||||
// ****************************************************************************
|
|
||||||
// SectionHeader (TODO: DRY)
|
|
||||||
// ****************************************************************************
|
|
||||||
|
|
||||||
type SectionHeaderProps = {
|
|
||||||
title: string,
|
|
||||||
navigate?: string,
|
|
||||||
icon?: string,
|
|
||||||
help?: string,
|
|
||||||
onHide?: () => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SectionHeader = ({ title, icon = '', help, onHide }: SectionHeaderProps) => {
|
|
||||||
const SHARE_DOMAIN = SHARE_DOMAIN_URL || URL;
|
|
||||||
return (
|
|
||||||
<h1 className="claim-grid__header">
|
|
||||||
<Icon className="claim-grid__header-icon" sectionIcon icon={icon} size={20} />
|
|
||||||
<span className="claim-grid__title">{title}</span>
|
|
||||||
{help}
|
|
||||||
<HelpLink href={`${SHARE_DOMAIN}/$/${PAGES.FYP}`} iconSize={24} description={__('Learn more')} />
|
|
||||||
<Button button="link" label={__('Hide')} onClick={onHide} className={'ml-m text-s'} />
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
// RecommendedPersonal
|
// RecommendedPersonal
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
@ -50,28 +22,17 @@ function getSuitablePageSizeForScreen(defaultSize, isLargeScreen, isMediumScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
header: React$Node,
|
||||||
onLoad: (displayed: boolean) => void,
|
onLoad: (displayed: boolean) => void,
|
||||||
// --- redux ---
|
// --- redux ---
|
||||||
userId: ?string,
|
userId: ?string,
|
||||||
personalRecommendations: { gid: string, uris: Array<string> },
|
personalRecommendations: { gid: string, uris: Array<string> },
|
||||||
hasMembership: boolean,
|
hasMembership: boolean,
|
||||||
hideFyp: boolean,
|
|
||||||
doFetchPersonalRecommendations: () => void,
|
doFetchPersonalRecommendations: () => void,
|
||||||
doSetClientSetting: (key: string, value: any, pushPreferences: boolean) => void,
|
|
||||||
doToast: ({ isError?: boolean, message: string }) => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RecommendedPersonal(props: Props) {
|
export default function RecommendedPersonal(props: Props) {
|
||||||
const {
|
const { header, onLoad, userId, personalRecommendations, hasMembership, doFetchPersonalRecommendations } = props;
|
||||||
onLoad,
|
|
||||||
userId,
|
|
||||||
personalRecommendations,
|
|
||||||
hasMembership,
|
|
||||||
hideFyp,
|
|
||||||
doFetchPersonalRecommendations,
|
|
||||||
doSetClientSetting,
|
|
||||||
doToast,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const ref = React.useRef();
|
const ref = React.useRef();
|
||||||
const [markedGid, setMarkedGid] = React.useState('');
|
const [markedGid, setMarkedGid] = React.useState('');
|
||||||
|
@ -83,11 +44,6 @@ export default function RecommendedPersonal(props: Props) {
|
||||||
const countCollapsed = getSuitablePageSizeForScreen(8, isLargeScreen, isMediumScreen);
|
const countCollapsed = getSuitablePageSizeForScreen(8, isLargeScreen, isMediumScreen);
|
||||||
const finalCount = view === VIEW.ALL_VISIBLE ? count : view === VIEW.COLLAPSED ? countCollapsed : count;
|
const finalCount = view === VIEW.ALL_VISIBLE ? count : view === VIEW.COLLAPSED ? countCollapsed : count;
|
||||||
|
|
||||||
function doHideFyp() {
|
|
||||||
doSetClientSetting(SETTINGS.HIDE_FYP, true, true);
|
|
||||||
doToast({ message: __('Recommendations hidden; you can re-enable them in Settings.') });
|
|
||||||
}
|
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// Effects
|
// Effects
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
@ -133,13 +89,39 @@ export default function RecommendedPersonal(props: Props) {
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
if (hideFyp || !hasMembership || count < 1) {
|
if (!hasMembership) {
|
||||||
return null;
|
return (
|
||||||
|
<div>
|
||||||
|
{header}
|
||||||
|
<div className="empty empty--centered-tight">
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{ learn_more: <Button button="link" navigate={`/$/${PAGES.FYP}`} label={__('learn more')} /> }}
|
||||||
|
>
|
||||||
|
Premium membership required. Become a member, or %learn_more%.
|
||||||
|
</I18nMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < 1) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{header}
|
||||||
|
<div className="empty empty--centered-tight">
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{ learn_more: <Button button="link" navigate={`/$/${PAGES.FYP}`} label={__('Learn More')} /> }}
|
||||||
|
>
|
||||||
|
No recommendations available at the moment. %learn_more%
|
||||||
|
</I18nMessage>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<SectionHeader title={__('Recommended For You')} icon={ICONS.WEB} onHide={doHideFyp} />
|
{header}
|
||||||
|
|
||||||
<ClaimList
|
<ClaimList
|
||||||
tileLayout
|
tileLayout
|
||||||
|
@ -162,8 +144,7 @@ export default function RecommendedPersonal(props: Props) {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
ref.current.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||||
} else {
|
} else {
|
||||||
// Unlikely to happen, but should have a fallback (just go all the way up)
|
window.scrollTo({ top: 0, behavior: 'smooth' }); // fallback, unlikely.
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export const CONFIRM = 'confirm';
|
export const CONFIRM = 'confirm';
|
||||||
export const CONFIRM_FILE_REMOVE = 'confirm_file_remove';
|
export const CONFIRM_FILE_REMOVE = 'confirm_file_remove';
|
||||||
export const CONFIRM_EXTERNAL_RESOURCE = 'confirm_external_resource';
|
export const CONFIRM_EXTERNAL_RESOURCE = 'confirm_external_resource';
|
||||||
|
export const CUSTOMIZE_HOMEPAGE = 'customize_homepage';
|
||||||
export const INCOMPATIBLE_DAEMON = 'incompatible_daemon';
|
export const INCOMPATIBLE_DAEMON = 'incompatible_daemon';
|
||||||
export const FILE_TIMEOUT = 'file_timeout';
|
export const FILE_TIMEOUT = 'file_timeout';
|
||||||
export const FILE_SELECTION = 'file_selection';
|
export const FILE_SELECTION = 'file_selection';
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const TAGS_ACKNOWLEDGED = 'tags_acknowledged';
|
||||||
export const REWARDS_ACKNOWLEDGED = 'rewards_acknowledged';
|
export const REWARDS_ACKNOWLEDGED = 'rewards_acknowledged';
|
||||||
export const SEARCH_IN_LANGUAGE = 'search_in_language';
|
export const SEARCH_IN_LANGUAGE = 'search_in_language';
|
||||||
export const HOMEPAGE = 'homepage';
|
export const HOMEPAGE = 'homepage';
|
||||||
|
export const HOMEPAGE_ORDER = 'homepage_order';
|
||||||
export const HIDE_REPOSTS = 'hide_reposts';
|
export const HIDE_REPOSTS = 'hide_reposts';
|
||||||
export const HIDE_SCHEDULED_LIVESTREAMS = 'hide_scheduled_livestreams';
|
export const HIDE_SCHEDULED_LIVESTREAMS = 'hide_scheduled_livestreams';
|
||||||
export const HIDE_FYP = 'hide_fyp';
|
export const HIDE_FYP = 'hide_fyp';
|
||||||
|
|
|
@ -31,4 +31,5 @@ export const CLIENT_SYNC_KEYS = [
|
||||||
SETTINGS.DARK_MODE_TIMES,
|
SETTINGS.DARK_MODE_TIMES,
|
||||||
SETTINGS.AUTOMATIC_DARK_MODE_ENABLED,
|
SETTINGS.AUTOMATIC_DARK_MODE_ENABLED,
|
||||||
SETTINGS.LANGUAGE,
|
SETTINGS.LANGUAGE,
|
||||||
|
SETTINGS.HOMEPAGE_ORDER,
|
||||||
];
|
];
|
||||||
|
|
19
ui/modal/modalCustomizeHomepage/index.js
Normal file
19
ui/modal/modalCustomizeHomepage/index.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ModalCustomizeHomepage from './view';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { doHideModal } from 'redux/actions/app';
|
||||||
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { selectHasOdyseeMembership } from 'redux/selectors/user';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
hasMembership: selectHasOdyseeMembership(state),
|
||||||
|
homepageOrder: selectClientSetting(state, SETTINGS.HOMEPAGE_ORDER),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = {
|
||||||
|
doSetClientSetting,
|
||||||
|
doHideModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(select, perform)(ModalCustomizeHomepage);
|
93
ui/modal/modalCustomizeHomepage/view.jsx
Normal file
93
ui/modal/modalCustomizeHomepage/view.jsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import HomepageSort from 'component/homepageSort';
|
||||||
|
import MembershipSplash from 'component/membershipSplash';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { Modal } from 'modal/modal';
|
||||||
|
|
||||||
|
type HomepageOrder = { active: ?Array<string>, hidden: ?Array<string> };
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hasMembership: ?boolean,
|
||||||
|
homepageOrder: HomepageOrder,
|
||||||
|
doSetClientSetting: (key: string, value: any, push: boolean) => void,
|
||||||
|
doHideModal: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ModalCustomizeHomepage(props: Props) {
|
||||||
|
const { hasMembership, homepageOrder, doSetClientSetting, doHideModal } = props;
|
||||||
|
const order = React.useRef();
|
||||||
|
|
||||||
|
function handleNewOrder(newOrder: HomepageOrder) {
|
||||||
|
order.current = newOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
// Non-English homepages created their own categories, so that made things
|
||||||
|
// complicated. Store every new key encountered, and just not show them
|
||||||
|
// in the GUI depending on the selected homepage language.
|
||||||
|
// Be sure not to erase any saved keys.
|
||||||
|
if (order.current) {
|
||||||
|
const orderToSave: HomepageOrder = order.current;
|
||||||
|
|
||||||
|
// ** Note: the forEach() is probably masking Flow from seeing that null active/hidden is already handled.
|
||||||
|
if (orderToSave.active && orderToSave.hidden) {
|
||||||
|
if (homepageOrder.active) {
|
||||||
|
homepageOrder.active.forEach((x) => {
|
||||||
|
// $FlowFixMe: **
|
||||||
|
if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) {
|
||||||
|
// $FlowFixMe: **
|
||||||
|
orderToSave.active.push(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (homepageOrder.hidden) {
|
||||||
|
homepageOrder.hidden.forEach((x) => {
|
||||||
|
// $FlowFixMe: **
|
||||||
|
if (!orderToSave.active.includes(x) && !orderToSave.hidden.includes(x)) {
|
||||||
|
// $FlowFixMe: **
|
||||||
|
orderToSave.hidden.push(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doSetClientSetting(SETTINGS.HOMEPAGE_ORDER, orderToSave, true);
|
||||||
|
} else {
|
||||||
|
console.error('Homepage: invalid orderToSave', orderToSave); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doHideModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen type={hasMembership ? 'custom' : 'card'} onAborted={hasMembership ? undefined : doHideModal}>
|
||||||
|
{!hasMembership && (
|
||||||
|
<Card
|
||||||
|
title={__('Customize Homepage')}
|
||||||
|
subtitle={__('This is currently an early-access feature for Premium members.')}
|
||||||
|
body={
|
||||||
|
<div className="card__main-actions">
|
||||||
|
<MembershipSplash pageLocation={'confirmPage'} currencyToUse={'usd'} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasMembership && (
|
||||||
|
<Card
|
||||||
|
title={__('Customize Homepage')}
|
||||||
|
body={<HomepageSort onUpdate={handleNewOrder} />}
|
||||||
|
actions={
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button button="primary" label={__('Save')} onClick={handleSave} />
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={doHideModal} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ const MAP = Object.freeze({
|
||||||
[MODALS.CONFIRM_REMOVE_COMMENT]: lazyImport(() => import('modal/modalRemoveComment' /* webpackChunkName: "modalRemoveComment" */)),
|
[MODALS.CONFIRM_REMOVE_COMMENT]: lazyImport(() => import('modal/modalRemoveComment' /* webpackChunkName: "modalRemoveComment" */)),
|
||||||
[MODALS.CONFIRM_THUMBNAIL_UPLOAD]: lazyImport(() => import('modal/modalConfirmThumbnailUpload' /* webpackChunkName: "modalConfirmThumbnailUpload" */)),
|
[MODALS.CONFIRM_THUMBNAIL_UPLOAD]: lazyImport(() => import('modal/modalConfirmThumbnailUpload' /* webpackChunkName: "modalConfirmThumbnailUpload" */)),
|
||||||
[MODALS.CONFIRM_TRANSACTION]: lazyImport(() => import('modal/modalConfirmTransaction' /* webpackChunkName: "modalConfirmTransaction" */)),
|
[MODALS.CONFIRM_TRANSACTION]: lazyImport(() => import('modal/modalConfirmTransaction' /* webpackChunkName: "modalConfirmTransaction" */)),
|
||||||
|
[MODALS.CUSTOMIZE_HOMEPAGE]: lazyImport(() => import('modal/modalCustomizeHomepage' /* webpackChunkName: "modalCustomizeHomepage" */)),
|
||||||
[MODALS.DOWNLOADING]: lazyImport(() => import('modal/modalDownloading' /* webpackChunkName: "modalDownloading" */)),
|
[MODALS.DOWNLOADING]: lazyImport(() => import('modal/modalDownloading' /* webpackChunkName: "modalDownloading" */)),
|
||||||
[MODALS.ERROR]: lazyImport(() => import('modal/modalError' /* webpackChunkName: "modalError" */)),
|
[MODALS.ERROR]: lazyImport(() => import('modal/modalError' /* webpackChunkName: "modalError" */)),
|
||||||
[MODALS.FILE_SELECTION]: lazyImport(() => import('modal/modalFileSelection' /* webpackChunkName: "modalFileSelection" */)),
|
[MODALS.FILE_SELECTION]: lazyImport(() => import('modal/modalFileSelection' /* webpackChunkName: "modalFileSelection" */)),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||||
import { selectAdBlockerFound } from 'redux/selectors/app';
|
import { selectAdBlockerFound } from 'redux/selectors/app';
|
||||||
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
|
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
|
||||||
|
@ -20,10 +21,12 @@ const select = (state) => ({
|
||||||
fetchingActiveLivestreams: selectFetchingActiveLivestreams(state),
|
fetchingActiveLivestreams: selectFetchingActiveLivestreams(state),
|
||||||
hideScheduledLivestreams: selectClientSetting(state, SETTINGS.HIDE_SCHEDULED_LIVESTREAMS),
|
hideScheduledLivestreams: selectClientSetting(state, SETTINGS.HIDE_SCHEDULED_LIVESTREAMS),
|
||||||
adBlockerFound: selectAdBlockerFound(state),
|
adBlockerFound: selectAdBlockerFound(state),
|
||||||
|
homepageOrder: selectClientSetting(state, SETTINGS.HOMEPAGE_ORDER),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
doFetchActiveLivestreams: () => dispatch(doFetchActiveLivestreams()),
|
doFetchActiveLivestreams: () => dispatch(doFetchActiveLivestreams()),
|
||||||
|
doOpenModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(DiscoverPage);
|
export default connect(select, perform)(DiscoverPage);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { SITE_NAME, SIMPLE_SITE, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
@ -10,6 +11,7 @@ import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
||||||
import RecommendedPersonal from 'component/recommendedPersonal';
|
import RecommendedPersonal from 'component/recommendedPersonal';
|
||||||
|
import Yrbl from 'component/yrbl';
|
||||||
import { useIsLargeScreen } from 'effects/use-screensize';
|
import { useIsLargeScreen } from 'effects/use-screensize';
|
||||||
import { GetLinksData } from 'util/buildHomepage';
|
import { GetLinksData } from 'util/buildHomepage';
|
||||||
import { getLivestreamUris } from 'util/livestream';
|
import { getLivestreamUris } from 'util/livestream';
|
||||||
|
@ -17,15 +19,14 @@ import ScheduledStreams from 'component/scheduledStreams';
|
||||||
import { splitBySeparator } from 'util/lbryURI';
|
import { splitBySeparator } from 'util/lbryURI';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Ads from 'web/component/ads';
|
import Ads from 'web/component/ads';
|
||||||
|
|
||||||
// @if TARGET='web'
|
|
||||||
import Meme from 'web/component/meme';
|
import Meme from 'web/component/meme';
|
||||||
// @endif
|
|
||||||
|
|
||||||
function resolveTitleOverride(title: string) {
|
function resolveTitleOverride(title: string) {
|
||||||
return title === 'Recent From Following' ? 'Following' : title;
|
return title === 'Recent From Following' ? 'Following' : title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HomepageOrder = { active: ?Array<string>, hidden: ?Array<string> };
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
followedTags: Array<Tag>,
|
followedTags: Array<Tag>,
|
||||||
|
@ -37,6 +38,8 @@ type Props = {
|
||||||
fetchingActiveLivestreams: boolean,
|
fetchingActiveLivestreams: boolean,
|
||||||
hideScheduledLivestreams: boolean,
|
hideScheduledLivestreams: boolean,
|
||||||
adBlockerFound: ?boolean,
|
adBlockerFound: ?boolean,
|
||||||
|
homepageOrder: HomepageOrder,
|
||||||
|
doOpenModal: (id: string, ?{}) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function HomePage(props: Props) {
|
function HomePage(props: Props) {
|
||||||
|
@ -51,6 +54,8 @@ function HomePage(props: Props) {
|
||||||
fetchingActiveLivestreams,
|
fetchingActiveLivestreams,
|
||||||
hideScheduledLivestreams,
|
hideScheduledLivestreams,
|
||||||
adBlockerFound,
|
adBlockerFound,
|
||||||
|
homepageOrder,
|
||||||
|
doOpenModal,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
||||||
|
@ -72,6 +77,26 @@ function HomePage(props: Props) {
|
||||||
showNsfw
|
showNsfw
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: probably need memo, or incorporate into GetLinksData.
|
||||||
|
let sortedRowData: Array<RowDataItem> = [];
|
||||||
|
if (homepageOrder.active) {
|
||||||
|
homepageOrder.active.forEach((key) => {
|
||||||
|
const item = rowData.find((data) => data.id === key);
|
||||||
|
if (item) {
|
||||||
|
sortedRowData.push(item);
|
||||||
|
} else if (key === 'FYP') {
|
||||||
|
sortedRowData.push({
|
||||||
|
id: 'FYP',
|
||||||
|
title: 'Recommended For You',
|
||||||
|
icon: ICONS.GLOBE,
|
||||||
|
link: `/$/${PAGES.FYP}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sortedRowData = rowData;
|
||||||
|
}
|
||||||
|
|
||||||
type SectionHeaderProps = {
|
type SectionHeaderProps = {
|
||||||
title: string,
|
title: string,
|
||||||
navigate?: string,
|
navigate?: string,
|
||||||
|
@ -91,7 +116,19 @@ function HomePage(props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getRowElements(title, route, link, icon, help, options, index, pinUrls) {
|
const CustomizeHomepage = () => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
iconRight={ICONS.SETTINGS}
|
||||||
|
onClick={() => doOpenModal(MODALS.CUSTOMIZE_HOMEPAGE)}
|
||||||
|
title={__('Sort and customize your homepage')}
|
||||||
|
label={__('Customize --[Short label for "Customize Homepage"]--')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRowElements(id, title, route, link, icon, help, options, index, pinUrls) {
|
||||||
const tilePlaceholder = (
|
const tilePlaceholder = (
|
||||||
<ul className="claim-grid">
|
<ul className="claim-grid">
|
||||||
{new Array(options.pageSize || 8).fill(1).map((x, i) => (
|
{new Array(options.pageSize || 8).fill(1).map((x, i) => (
|
||||||
|
@ -116,34 +153,48 @@ function HomePage(props: Props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFollowingSection = link === `/$/${PAGES.CHANNELS_FOLLOWING}`;
|
const HeaderArea = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{title && typeof title === 'string' && (
|
||||||
|
<div className="homePage-wrapper__section-title">
|
||||||
|
<SectionHeader title={__(resolveTitleOverride(title))} navigate={route || link} icon={icon} help={help} />
|
||||||
|
{index === 0 && <CustomizeHomepage />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={title}
|
key={title}
|
||||||
className={classnames('claim-grid__wrapper', {
|
className={classnames('claim-grid__wrapper', {
|
||||||
'hide-ribbon': !isFollowingSection,
|
'hide-ribbon': link !== `/$/${PAGES.CHANNELS_FOLLOWING}`,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{title && typeof title === 'string' && (
|
{id === 'FYP' ? (
|
||||||
<SectionHeader title={__(resolveTitleOverride(title))} navigate={route || link} icon={icon} help={help} />
|
<RecommendedPersonal header={<HeaderArea />} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<HeaderArea />
|
||||||
|
{index === 0 && <>{claimTiles}</>}
|
||||||
|
{index !== 0 && (
|
||||||
|
<WaitUntilOnPage name={title} placeholder={tilePlaceholder} yOffset={800}>
|
||||||
|
{claimTiles}
|
||||||
|
</WaitUntilOnPage>
|
||||||
|
)}
|
||||||
|
{(route || link) && (
|
||||||
|
<Button
|
||||||
|
className="claim-grid__title--secondary"
|
||||||
|
button="link"
|
||||||
|
navigate={route || link}
|
||||||
|
iconRight={ICONS.ARROW_RIGHT}
|
||||||
|
label={__('View More')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{index === 0 && <>{claimTiles}</>}
|
|
||||||
{index !== 0 && (
|
|
||||||
<WaitUntilOnPage name={title} placeholder={tilePlaceholder} yOffset={800}>
|
|
||||||
{claimTiles}
|
|
||||||
</WaitUntilOnPage>
|
|
||||||
)}
|
|
||||||
{(route || link) && (
|
|
||||||
<Button
|
|
||||||
className="claim-grid__title--secondary"
|
|
||||||
button="link"
|
|
||||||
navigate={route || link}
|
|
||||||
iconRight={ICONS.ARROW_RIGHT}
|
|
||||||
label={__('View More')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isFollowingSection && <RecommendedPersonal />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -154,24 +205,7 @@ function HomePage(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page className="homePage-wrapper" fullWidthPage>
|
<Page className="homePage-wrapper" fullWidthPage>
|
||||||
{!SIMPLE_SITE && (authenticated || !IS_WEB) && !subscribedChannels.length && (
|
<Meme />
|
||||||
<div className="notice-message">
|
|
||||||
<h1 className="section__title">
|
|
||||||
{__("%SITE_NAME% is more fun if you're following channels", { SITE_NAME })}
|
|
||||||
</h1>
|
|
||||||
<p className="section__actions">
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
|
|
||||||
label={__('Find new channels to follow')}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* @if TARGET='web' */}
|
|
||||||
{SIMPLE_SITE && <Meme />}
|
|
||||||
{/* @endif */}
|
|
||||||
|
|
||||||
{!fetchingActiveLivestreams && (
|
{!fetchingActiveLivestreams && (
|
||||||
<>
|
<>
|
||||||
|
@ -186,9 +220,18 @@ function HomePage(props: Props) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{rowData.map(({ title, route, link, icon, help, pinnedUrls: pinUrls, options = {} }, index) => {
|
{sortedRowData.length === 0 && (
|
||||||
// add pins here
|
<div className="empty--centered">
|
||||||
return getRowElements(title, route, link, icon, help, options, index, pinUrls);
|
<Yrbl
|
||||||
|
alwaysShow
|
||||||
|
title={__('Clean as a whistle! --[title for empty homepage]--')}
|
||||||
|
actions={<CustomizeHomepage />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sortedRowData.map(({ id, title, route, link, icon, help, pinnedUrls: pinUrls, options = {} }, index) => {
|
||||||
|
return getRowElements(id, title, route, link, icon, help, options, index, pinUrls);
|
||||||
})}
|
})}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -43,6 +43,7 @@ const defaultState = {
|
||||||
[SETTINGS.THEME]: __('dark'),
|
[SETTINGS.THEME]: __('dark'),
|
||||||
[SETTINGS.THEMES]: [__('dark'), __('light')],
|
[SETTINGS.THEMES]: [__('dark'), __('light')],
|
||||||
[SETTINGS.HOMEPAGE]: null,
|
[SETTINGS.HOMEPAGE]: null,
|
||||||
|
[SETTINGS.HOMEPAGE_ORDER]: { active: null, hidden: null },
|
||||||
[SETTINGS.HIDE_SPLASH_ANIMATION]: false,
|
[SETTINGS.HIDE_SPLASH_ANIMATION]: false,
|
||||||
[SETTINGS.HIDE_BALANCE]: false,
|
[SETTINGS.HIDE_BALANCE]: false,
|
||||||
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: true,
|
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: true,
|
||||||
|
|
55
ui/scss/component/homepage-sort.scss
Normal file
55
ui/scss/component/homepage-sort.scss
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
.homepage-sort {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage-sort__bin {
|
||||||
|
width: 50%;
|
||||||
|
background-color: var(--color-ads-background);
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage-sort__bin--dashed {
|
||||||
|
border: dashed var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage-sort__bin--highlight {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage-sort__bin--no-bg {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage-sort__bin-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
color: var(--color-text-subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage-sort__entry {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid grey;
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
background-color: var(--color-card-background);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-m) var(--spacing-xs) var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-small);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-top: var(--spacing-xxs);
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.homepage-sort__entry:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
|
@ -623,6 +623,11 @@ img {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.homePage-wrapper__section-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
// The following wrapper classes are temporary to fix page specific issues
|
// The following wrapper classes are temporary to fix page specific issues
|
||||||
.discoverPage-wrapper {
|
.discoverPage-wrapper {
|
||||||
.claim-preview__wrapper {
|
.claim-preview__wrapper {
|
||||||
|
|
Loading…
Reference in a new issue