limit collection display

make it clear lists page is truncated

lists display page

cleanup
This commit is contained in:
zeppi 2021-10-27 21:57:24 -04:00 committed by jessopb
parent 7a2adae09c
commit 5df736dc6b
12 changed files with 320 additions and 74 deletions

View file

@ -2216,5 +2216,8 @@
"Enable Data Hosting": "Enable Data Hosting", "Enable Data Hosting": "Enable Data Hosting",
"Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.": "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.", "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.": "Data over the limit will be deleted within 30 minutes. This will make the Yrbl cry a little bit.",
"Choose %asset%": "Choose %asset%", "Choose %asset%": "Choose %asset%",
"Showing %filtered% results of %total%": "Showing %filtered% results of %total%",
"filtered": "filtered",
"View All Playlists": "View All Playlists",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -25,6 +25,7 @@ const ALL = 'All';
const PRIVATE = 'Private'; const PRIVATE = 'Private';
const PUBLIC = 'Public'; const PUBLIC = 'Public';
const COLLECTION_FILTERS = [ALL, PRIVATE, PUBLIC]; const COLLECTION_FILTERS = [ALL, PRIVATE, PUBLIC];
const COLLECTION_SHOW_COUNT = 12;
export default function CollectionsListMine(props: Props) { export default function CollectionsListMine(props: Props) {
const { const {
@ -53,18 +54,24 @@ export default function CollectionsListMine(props: Props) {
let filteredCollections; let filteredCollections;
if (searchText && collectionsToShow) { if (searchText && collectionsToShow) {
filteredCollections = collectionsToShow.filter((id) => { filteredCollections = collectionsToShow
return ( .filter((id) => {
(unpublishedCollections[id] && return (
unpublishedCollections[id].name.toLocaleLowerCase().includes(searchText.toLocaleLowerCase())) || (unpublishedCollections[id] &&
(publishedCollections[id] && unpublishedCollections[id].name.toLocaleLowerCase().includes(searchText.toLocaleLowerCase())) ||
publishedCollections[id].name.toLocaleLowerCase().includes(searchText.toLocaleLowerCase())) (publishedCollections[id] &&
); publishedCollections[id].name.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()))
}); );
})
.slice(0, COLLECTION_SHOW_COUNT);
} else { } else {
filteredCollections = collectionsToShow || []; filteredCollections = collectionsToShow.slice(0, COLLECTION_SHOW_COUNT) || [];
} }
const totalLength = collectionsToShow ? collectionsToShow.length : 0;
const filteredLength = filteredCollections.length;
const isTruncated = totalLength > filteredLength;
const watchLater = builtinCollectionsList.find((list) => list.id === COLLECTIONS_CONSTS.WATCH_LATER_ID); const watchLater = builtinCollectionsList.find((list) => list.id === COLLECTIONS_CONSTS.WATCH_LATER_ID);
const favorites = builtinCollectionsList.find((list) => list.id === COLLECTIONS_CONSTS.FAVORITES_ID); const favorites = builtinCollectionsList.find((list) => list.id === COLLECTIONS_CONSTS.FAVORITES_ID);
const builtin = [watchLater, favorites]; const builtin = [watchLater, favorites];
@ -130,7 +137,19 @@ export default function CollectionsListMine(props: Props) {
<div className="claim-grid__wrapper"> <div className="claim-grid__wrapper">
<div className="claim-grid__header section"> <div className="claim-grid__header section">
<h1 className="claim-grid__title"> <h1 className="claim-grid__title">
{__('Playlists')} <Button
className="claim-grid__title"
button="link"
navigate={`/$/${PAGES.PLAYLISTS}`}
label={
<span className="claim-grid__title-span">
{__('Playlists')}
<div className="claim-grid__title--empty">
<Icon className="icon--margin-right" icon={ICONS.STACK} />
</div>
</span>
}
/>
{!hasCollections && !fetchingCollections && ( {!hasCollections && !fetchingCollections && (
<div className="claim-grid__title--empty">{__('(Empty) --[indicates empty playlist]--')}</div> <div className="claim-grid__title--empty">{__('(Empty) --[indicates empty playlist]--')}</div>
)} )}
@ -139,38 +158,53 @@ export default function CollectionsListMine(props: Props) {
)} )}
</h1> </h1>
</div> </div>
<div className="section__header--actions"> <div className="section__header-action-stack">
<div className="claim-search__wrapper"> <div className="section__header--actions">
<div className="claim-search__menu-group"> <div className="claim-search__wrapper">
{COLLECTION_FILTERS.map((value) => ( <div className="claim-search__menu-group">
<Button {COLLECTION_FILTERS.map((value) => (
label={__(value)} <Button
key={value} label={__(value)}
button="alt" key={value}
onClick={() => setFilterType(value)} button="alt"
className={classnames('button-toggle', { onClick={() => setFilterType(value)}
'button-toggle--active': filterType === value, className={classnames('button-toggle', {
})} 'button-toggle--active': filterType === value,
/> })}
))} />
))}
</div>
</div> </div>
<Form onSubmit={() => {}} className="wunderbar--inline">
<Icon icon={ICONS.SEARCH} />
<FormField
onFocus={onTextareaFocus}
onBlur={onTextareaBlur}
className="wunderbar__input--inline"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
type="text"
placeholder={__('Search')}
/>
</Form>
</div> </div>
<Form onSubmit={() => {}} className="wunderbar--inline"> {isTruncated && (
<Icon icon={ICONS.SEARCH} /> <p className="collection-grid__results-summary">
<FormField {__(`Showing %filtered% results of %total%`, {
onFocus={onTextareaFocus} filtered: filteredLength,
onBlur={onTextareaBlur} total: totalLength,
className="wunderbar__input--inline" })}
value={searchText} {`${searchText ? ' (' + __('filtered') + ') ' : ' '}`}
onChange={(e) => setSearchText(e.target.value)} <Button
type="text" button="link"
placeholder={__('Search')} navigate={`/$/${PAGES.PLAYLISTS}`}
/> label={<span className="claim-grid__title-span">{__('View All Playlists')}</span>}
</Form> />
</p>
)}
</div> </div>
{Boolean(hasCollections) && ( {Boolean(hasCollections) && (
<div> <div>
{/* TODO: fix above spacing hack */}
<div className="claim-grid"> <div className="claim-grid">
{filteredCollections && {filteredCollections &&
filteredCollections.length > 0 && filteredCollections.length > 0 &&

View file

@ -0,0 +1,29 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import {
selectMyPublishedPlaylistCollections,
selectMyUnpublishedCollections, // should probably distinguish types
// selectSavedCollections,
} from 'redux/selectors/collections';
import { selectFetchingMyCollections } from 'redux/selectors/claims';
import PlaylistsMine from './view';
import { PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim';
const COLLECTIONS_PAGE_SIZE = 12;
const select = (state, props) => {
const { search } = props.location;
const urlParams = new URLSearchParams(search);
const page = Number(urlParams.get(PAGE_PARAM)) || '1';
const pageSize = urlParams.get(PAGE_SIZE_PARAM) || String(COLLECTIONS_PAGE_SIZE);
return {
page,
pageSize,
publishedCollections: selectMyPublishedPlaylistCollections(state),
unpublishedCollections: selectMyUnpublishedCollections(state),
// savedCollections: selectSavedCollections(state),
fetchingCollections: selectFetchingMyCollections(state),
};
};
export default withRouter(connect(select)(PlaylistsMine));

View file

@ -0,0 +1,161 @@
// @flow
import React from 'react';
import CollectionPreviewTile from 'component/collectionPreviewTile';
import Button from 'component/button';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
import * as KEYCODES from 'constants/keycodes';
import Paginate from 'component/common/paginate';
import Yrbl from 'component/yrbl';
import classnames from 'classnames';
import { FormField, Form } from 'component/common/form';
type Props = {
publishedCollections: CollectionGroup,
unpublishedCollections: CollectionGroup,
// savedCollections: CollectionGroup,
fetchingCollections: boolean,
page: number,
pageSize: number,
};
const ALL = 'All';
const PRIVATE = 'Private';
const PUBLIC = 'Public';
const COLLECTION_FILTERS = [ALL, PRIVATE, PUBLIC];
export default function PlaylistsMine(props: Props) {
const {
publishedCollections,
unpublishedCollections,
// savedCollections, these are resolved on startup from sync'd claimIds or urls
fetchingCollections,
page = 0,
pageSize,
} = props;
const unpublishedCollectionsList = (Object.keys(unpublishedCollections || {}): any);
const publishedList = (Object.keys(publishedCollections || {}): any);
const hasCollections = unpublishedCollectionsList.length || publishedList.length;
const [filterType, setFilterType] = React.useState(ALL);
const [searchText, setSearchText] = React.useState('');
let collectionsToShow = [];
if (filterType === ALL) {
collectionsToShow = unpublishedCollectionsList.concat(publishedList);
} else if (filterType === PRIVATE) {
collectionsToShow = unpublishedCollectionsList;
} else if (filterType === PUBLIC) {
collectionsToShow = publishedList;
}
let filteredCollections;
if (searchText && collectionsToShow) {
filteredCollections = collectionsToShow.filter((id) => {
return (
(unpublishedCollections[id] &&
unpublishedCollections[id].name.toLocaleLowerCase().includes(searchText.toLocaleLowerCase())) ||
(publishedCollections[id] &&
publishedCollections[id].name.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()))
);
});
} else {
filteredCollections = collectionsToShow || [];
}
const shouldPaginate = filteredCollections.length > pageSize;
const paginateStart = shouldPaginate ? (page - 1) * pageSize : 0;
const paginatedCollections = filteredCollections.slice(paginateStart, paginateStart + pageSize);
function escapeListener(e: SyntheticKeyboardEvent<*>) {
if (e.keyCode === KEYCODES.ESCAPE) {
e.preventDefault();
setSearchText('');
}
}
function onTextareaFocus() {
window.addEventListener('keydown', escapeListener);
}
function onTextareaBlur() {
window.removeEventListener('keydown', escapeListener);
}
return (
<>
<div className="claim-grid__wrapper">
<div className="claim-grid__header section">
<h1 className="claim-grid__title">
{__('Playlists')}
{!hasCollections && !fetchingCollections && (
<div className="claim-grid__title--empty">{__('(Empty) --[indicates empty playlist]--')}</div>
)}
{!hasCollections && fetchingCollections && (
<div className="claim-grid__title--empty">{__('(Empty) --[indicates empty playlist]--')}</div>
)}
</h1>
</div>
<div className="section__header--actions">
<div className="claim-search__wrapper">
<div className="claim-search__menu-group">
{COLLECTION_FILTERS.map((value) => (
<Button
label={__(value)}
key={value}
button="alt"
onClick={() => setFilterType(value)}
className={classnames('button-toggle', {
'button-toggle--active': filterType === value,
})}
/>
))}
</div>
</div>
<Form onSubmit={() => {}} className="wunderbar--inline">
<Icon icon={ICONS.SEARCH} />
<FormField
onFocus={onTextareaFocus}
onBlur={onTextareaBlur}
className="wunderbar__input--inline"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
type="text"
placeholder={__('Search')}
/>
</Form>
</div>
{Boolean(hasCollections) && (
<div>
{/* TODO: fix above spacing hack */}
<div className="claim-grid">
{paginatedCollections &&
paginatedCollections.length > 0 &&
paginatedCollections.map((key) => <CollectionPreviewTile tileLayout collectionId={key} key={key} />)}
{!paginatedCollections.length && <div className="empty main--empty">{__('No matching playlists')}</div>}
</div>
{shouldPaginate && (
<Paginate
totalPages={
filteredCollections.length > 0 ? Math.ceil(filteredCollections.length / Number(pageSize)) : 1
}
/>
)}
</div>
)}
{!hasCollections && !fetchingCollections && (
<div className="main--empty">
<Yrbl type={'sad'} title={__('You have no lists yet. Better start hoarding!')} />
</div>
)}
{!hasCollections && fetchingCollections && (
<div className="main--empty">
<h2 className="main--empty empty">{__('Loading...')}</h2>
</div>
)}
</div>
</>
);
}

View file

@ -45,6 +45,7 @@ import InvitedPage from 'page/invited';
import LibraryPage from 'page/library'; import LibraryPage from 'page/library';
import ListBlockedPage from 'page/listBlocked'; import ListBlockedPage from 'page/listBlocked';
import ListsPage from 'page/lists'; import ListsPage from 'page/lists';
import PlaylistsPage from 'page/playlists';
import OwnComments from 'page/ownComments'; import OwnComments from 'page/ownComments';
import PasswordResetPage from 'page/passwordReset'; import PasswordResetPage from 'page/passwordReset';
import PasswordSetPage from 'page/passwordSet'; import PasswordSetPage from 'page/passwordSet';
@ -294,6 +295,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} /> <PrivateRoute {...props} path={`/$/${PAGES.REWARDS_VERIFY}`} component={RewardsVerifyPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LISTS}`} component={ListsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.LISTS}`} component={ListsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.PLAYLISTS}`} component={PlaylistsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} /> <PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />

View file

@ -27,6 +27,7 @@ exports.HOME = 'home';
exports.HELP = 'help'; exports.HELP = 'help';
exports.LIBRARY = 'library'; exports.LIBRARY = 'library';
exports.LISTS = 'lists'; exports.LISTS = 'lists';
exports.PLAYLISTS = 'playlists';
exports.INVITE = 'invite'; exports.INVITE = 'invite';
exports.DEPRECATED__PUBLISH = 'publish'; exports.DEPRECATED__PUBLISH = 'publish';
exports.DEPRECATED__PUBLISHED = 'published'; exports.DEPRECATED__PUBLISHED = 'published';

View file

@ -1,31 +1,3 @@
import { connect } from 'react-redux';
import { doPurchaseList } from 'redux/actions/claims';
import { selectMyPurchases, selectIsFetchingMyPurchases } from 'redux/selectors/claims';
import { selectDownloadUrlsCount, selectIsFetchingFileList } from 'redux/selectors/file_info';
import {
selectBuiltinCollections,
selectMyPublishedMixedCollections,
selectMyPublishedPlaylistCollections,
selectMyUnpublishedCollections, // should probably distinguish types
// selectSavedCollections, // TODO: implement saving and copying collections
} from 'redux/selectors/collections';
import ListsPage from './view'; import ListsPage from './view';
const select = (state) => ({ export default ListsPage;
allDownloadedUrlsCount: selectDownloadUrlsCount(state),
fetchingFileList: selectIsFetchingFileList(state),
myPurchases: selectMyPurchases(state),
fetchingMyPurchases: selectIsFetchingMyPurchases(state),
builtinCollections: selectBuiltinCollections(state),
publishedCollections: selectMyPublishedMixedCollections(state),
publishedPlaylists: selectMyPublishedPlaylistCollections(state),
unpublishedCollections: selectMyUnpublishedCollections(state),
// savedCollections: selectSavedCollections(state),
});
export default connect(select, {
doPurchaseList,
})(ListsPage);

View file

@ -0,0 +1,3 @@
import PlaylistsPage from './view';
export default PlaylistsPage;

View file

@ -0,0 +1,22 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import Page from 'component/page';
import PlaylistsMine from 'component/playlistsMine';
import Icon from 'component/common/icon';
function PlaylistsPage() {
return (
<Page>
<label className="claim-list__header-label">
<span>
<Icon icon={ICONS.STACK} size={10} />
{__('Playlists')}
</span>
</label>
<PlaylistsMine />
</Page>
);
}
export default PlaylistsPage;

View file

@ -136,3 +136,9 @@
} }
} }
} }
.collection-grid__results-summary {
padding: 0;
padding-bottom: var(--spacing-m);
color: var(--color-text-help);
}

View file

@ -313,14 +313,16 @@ fieldset-group {
} }
&:nth-of-type(2) { &:nth-of-type(2) {
input, &:not(input.paginate-channel) {
select { // yuck
border-top-left-radius: 0; input,
border-bottom-left-radius: 0; select {
} border-top-left-radius: 0;
border-bottom-left-radius: 0;
label { }
margin-left: var(--spacing-s); label {
margin-left: var(--spacing-s);
}
} }
} }
} }
@ -331,6 +333,12 @@ fieldset-group {
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
} }
&.fieldgroup--paginate-top {
padding-bottom: var(--spacing-m);
align-items: flex-end;
justify-content: center;
}
} }
// This is a special case where the prefix appears "inside" the input // This is a special case where the prefix appears "inside" the input

View file

@ -48,6 +48,11 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.section__header-action-stack {
display: flex;
flex-direction: column;
}
.section__flex { .section__flex {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;