Add horizontal layout (#636)
* Test out a horizontal scroll for upcoming (tile only for now) * - add support for list layout - add following label on home page - clan up css and naming conventions * Update header type + show only if scheduled streams are showing
This commit is contained in:
parent
58bdcbd1ed
commit
a89cb17ce4
10 changed files with 92 additions and 17 deletions
|
@ -47,6 +47,7 @@ type Props = {
|
||||||
maxClaimRender?: number,
|
maxClaimRender?: number,
|
||||||
excludeUris?: Array<string>,
|
excludeUris?: Array<string>,
|
||||||
loadedCallback?: (number) => void,
|
loadedCallback?: (number) => void,
|
||||||
|
swipeLayout: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ClaimList(props: Props) {
|
export default function ClaimList(props: Props) {
|
||||||
|
@ -80,6 +81,7 @@ export default function ClaimList(props: Props) {
|
||||||
maxClaimRender,
|
maxClaimRender,
|
||||||
excludeUris = [],
|
excludeUris = [],
|
||||||
loadedCallback,
|
loadedCallback,
|
||||||
|
swipeLayout = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||||
|
@ -148,7 +150,7 @@ export default function ClaimList(props: Props) {
|
||||||
}, [loading, onScrollBottom, urisLength, pageSize, page]);
|
}, [loading, onScrollBottom, urisLength, pageSize, page]);
|
||||||
|
|
||||||
return tileLayout && !header ? (
|
return tileLayout && !header ? (
|
||||||
<section className="claim-grid">
|
<section className={classnames('claim-grid', { 'swipe-list': swipeLayout })}>
|
||||||
{urisLength > 0 &&
|
{urisLength > 0 &&
|
||||||
tileUris.map((uri) => (
|
tileUris.map((uri) => (
|
||||||
<ClaimPreviewTile
|
<ClaimPreviewTile
|
||||||
|
@ -158,6 +160,7 @@ export default function ClaimList(props: Props) {
|
||||||
properties={renderProperties}
|
properties={renderProperties}
|
||||||
collectionId={collectionId}
|
collectionId={collectionId}
|
||||||
showNoSourceClaims={showNoSourceClaims}
|
showNoSourceClaims={showNoSourceClaims}
|
||||||
|
swipeLayout={swipeLayout}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
||||||
|
@ -200,8 +203,9 @@ export default function ClaimList(props: Props) {
|
||||||
{urisLength > 0 && (
|
{urisLength > 0 && (
|
||||||
<ul
|
<ul
|
||||||
className={classnames('ul--no-style', {
|
className={classnames('ul--no-style', {
|
||||||
card: !(tileLayout || type === 'small'),
|
card: !(tileLayout || swipeLayout || type === 'small'),
|
||||||
'claim-list--card-body': tileLayout,
|
'claim-list--card-body': tileLayout,
|
||||||
|
'swipe-list': swipeLayout,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{sortedUris.map((uri, index) => (
|
{sortedUris.map((uri, index) => (
|
||||||
|
@ -223,6 +227,7 @@ export default function ClaimList(props: Props) {
|
||||||
showNoSourceClaims={showNoSourceClaims}
|
showNoSourceClaims={showNoSourceClaims}
|
||||||
customShouldHide={customShouldHide}
|
customShouldHide={customShouldHide}
|
||||||
onClick={handleClaimClicked}
|
onClick={handleClaimClicked}
|
||||||
|
swipeLayout={swipeLayout}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -100,6 +100,8 @@ type Props = {
|
||||||
maxClaimRender?: number,
|
maxClaimRender?: number,
|
||||||
useSkeletonScreen?: boolean,
|
useSkeletonScreen?: boolean,
|
||||||
excludeUris?: Array<string>,
|
excludeUris?: Array<string>,
|
||||||
|
|
||||||
|
swipeLayout: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimListDiscover(props: Props) {
|
function ClaimListDiscover(props: Props) {
|
||||||
|
@ -169,6 +171,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
maxClaimRender,
|
maxClaimRender,
|
||||||
useSkeletonScreen = true,
|
useSkeletonScreen = true,
|
||||||
excludeUris = [],
|
excludeUris = [],
|
||||||
|
swipeLayout = false,
|
||||||
} = props;
|
} = props;
|
||||||
const didNavigateForward = history.action === 'PUSH';
|
const didNavigateForward = history.action === 'PUSH';
|
||||||
const { search } = location;
|
const { search } = location;
|
||||||
|
@ -634,6 +637,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
maxClaimRender={maxClaimRender}
|
maxClaimRender={maxClaimRender}
|
||||||
excludeUris={excludeUris}
|
excludeUris={excludeUris}
|
||||||
loadedCallback={loadedCallback}
|
loadedCallback={loadedCallback}
|
||||||
|
swipeLayout={swipeLayout}
|
||||||
/>
|
/>
|
||||||
{loading && useSkeletonScreen && (
|
{loading && useSkeletonScreen && (
|
||||||
<div className="claim-grid">
|
<div className="claim-grid">
|
||||||
|
@ -670,6 +674,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
maxClaimRender={maxClaimRender}
|
maxClaimRender={maxClaimRender}
|
||||||
excludeUris={excludeUris}
|
excludeUris={excludeUris}
|
||||||
loadedCallback={loadedCallback}
|
loadedCallback={loadedCallback}
|
||||||
|
swipeLayout={swipeLayout}
|
||||||
/>
|
/>
|
||||||
{loading &&
|
{loading &&
|
||||||
useSkeletonScreen &&
|
useSkeletonScreen &&
|
||||||
|
|
|
@ -86,6 +86,7 @@ type Props = {
|
||||||
date?: any,
|
date?: any,
|
||||||
indexInContainer?: number, // The index order of this component within 'containerId'.
|
indexInContainer?: number, // The index order of this component within 'containerId'.
|
||||||
channelSubCount?: number,
|
channelSubCount?: number,
|
||||||
|
swipeLayout: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
@ -146,6 +147,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
disableNavigation,
|
disableNavigation,
|
||||||
indexInContainer,
|
indexInContainer,
|
||||||
channelSubCount,
|
channelSubCount,
|
||||||
|
swipeLayout = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isCollection = claim && claim.value_type === 'collection';
|
const isCollection = claim && claim.value_type === 'collection';
|
||||||
|
@ -341,6 +343,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
'claim-preview--channel': isChannelUri,
|
'claim-preview--channel': isChannelUri,
|
||||||
'claim-preview--visited': !isChannelUri && !claimIsMine && hasVisitedUri,
|
'claim-preview--visited': !isChannelUri && !claimIsMine && hasVisitedUri,
|
||||||
'claim-preview--pending': pending,
|
'claim-preview--pending': pending,
|
||||||
|
'swipe-list__item': swipeLayout,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isChannelUri && claim ? (
|
{isChannelUri && claim ? (
|
||||||
|
|
|
@ -46,6 +46,7 @@ type Props = {
|
||||||
isLivestream: boolean,
|
isLivestream: boolean,
|
||||||
viewCount: string,
|
viewCount: string,
|
||||||
isLivestreamActive: boolean,
|
isLivestreamActive: boolean,
|
||||||
|
swipeLayout: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
// preview image cards used in related video functionality, channel overview page and homepage
|
// preview image cards used in related video functionality, channel overview page and homepage
|
||||||
|
@ -73,6 +74,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
collectionId,
|
collectionId,
|
||||||
mediaDuration,
|
mediaDuration,
|
||||||
viewCount,
|
viewCount,
|
||||||
|
swipeLayout = false,
|
||||||
} = props;
|
} = props;
|
||||||
const isRepost = claim && claim.repost_channel_url;
|
const isRepost = claim && claim.repost_channel_url;
|
||||||
const isCollection = claim && claim.value_type === 'collection';
|
const isCollection = claim && claim.value_type === 'collection';
|
||||||
|
@ -178,6 +180,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
className={classnames('card claim-preview--tile', {
|
className={classnames('card claim-preview--tile', {
|
||||||
'claim-preview__wrapper--channel': isChannel,
|
'claim-preview__wrapper--channel': isChannel,
|
||||||
'claim-preview__live': isLivestreamActive,
|
'claim-preview__live': isLivestreamActive,
|
||||||
|
'swipe-list__item claim-preview--horizontal-tile': swipeLayout,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
||||||
|
|
|
@ -16,13 +16,22 @@ type Props = {
|
||||||
tileLayout: boolean,
|
tileLayout: boolean,
|
||||||
liveUris: Array<string>,
|
liveUris: Array<string>,
|
||||||
limitClaimsPerChannel?: number,
|
limitClaimsPerChannel?: number,
|
||||||
|
onLoad: (number) => void,
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
setClientSetting: (string, boolean | string | number, boolean) => void,
|
setClientSetting: (string, boolean | string | number, boolean) => void,
|
||||||
doShowSnackBar: (string) => void,
|
doShowSnackBar: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ScheduledStreams = (props: Props) => {
|
const ScheduledStreams = (props: Props) => {
|
||||||
const { channelIds, tileLayout, liveUris = [], limitClaimsPerChannel, setClientSetting, doShowSnackBar } = props;
|
const {
|
||||||
|
channelIds,
|
||||||
|
tileLayout,
|
||||||
|
liveUris = [],
|
||||||
|
limitClaimsPerChannel,
|
||||||
|
setClientSetting,
|
||||||
|
doShowSnackBar,
|
||||||
|
onLoad,
|
||||||
|
} = props;
|
||||||
const isMediumScreen = useIsMediumScreen();
|
const isMediumScreen = useIsMediumScreen();
|
||||||
const isLargeScreen = useIsLargeScreen();
|
const isLargeScreen = useIsLargeScreen();
|
||||||
|
|
||||||
|
@ -30,16 +39,18 @@ const ScheduledStreams = (props: Props) => {
|
||||||
const [showAllUpcoming, setShowAllUpcoming] = React.useState(false);
|
const [showAllUpcoming, setShowAllUpcoming] = React.useState(false);
|
||||||
|
|
||||||
const showUpcomingLivestreams = totalUpcomingLivestreams > 0;
|
const showUpcomingLivestreams = totalUpcomingLivestreams > 0;
|
||||||
|
const useSwipeLayout = totalUpcomingLivestreams > 1 && isMediumScreen;
|
||||||
|
|
||||||
const upcomingMax = React.useMemo(() => {
|
const upcomingMax = React.useMemo(() => {
|
||||||
if (showAllUpcoming) return 50;
|
if (showAllUpcoming || useSwipeLayout) return 50;
|
||||||
if (isLargeScreen) return 6;
|
if (isLargeScreen) return 6;
|
||||||
if (isMediumScreen) return 3;
|
if (isMediumScreen) return 3;
|
||||||
return 4;
|
return 4;
|
||||||
}, [showAllUpcoming, isMediumScreen, isLargeScreen]);
|
}, [showAllUpcoming, isMediumScreen, isLargeScreen, useSwipeLayout]);
|
||||||
|
|
||||||
const loadedCallback = (total) => {
|
const loadedCallback = (total) => {
|
||||||
setTotalUpcomingLivestreams(total);
|
setTotalUpcomingLivestreams(total);
|
||||||
|
if (typeof onLoad === 'function') onLoad(total);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideScheduledStreams = () => {
|
const hideScheduledStreams = () => {
|
||||||
|
@ -57,8 +68,9 @@ const ScheduledStreams = (props: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'mb-xl'} style={{ display: showUpcomingLivestreams ? 'block' : 'none' }}>
|
<div className={'mb-m mt-m md:mb-xl'} style={{ display: showUpcomingLivestreams ? 'block' : 'none' }}>
|
||||||
<ClaimListDiscover
|
<ClaimListDiscover
|
||||||
|
swipeLayout={useSwipeLayout}
|
||||||
useSkeletonScreen={false}
|
useSkeletonScreen={false}
|
||||||
channelIds={channelIds}
|
channelIds={channelIds}
|
||||||
limitClaimsPerChannel={limitClaimsPerChannel}
|
limitClaimsPerChannel={limitClaimsPerChannel}
|
||||||
|
@ -78,7 +90,7 @@ const ScheduledStreams = (props: Props) => {
|
||||||
excludeUris={liveUris}
|
excludeUris={liveUris}
|
||||||
loadedCallback={loadedCallback}
|
loadedCallback={loadedCallback}
|
||||||
/>
|
/>
|
||||||
{totalUpcomingLivestreams > upcomingMax && !showAllUpcoming && (
|
{totalUpcomingLivestreams > upcomingMax && !showAllUpcoming && !useSwipeLayout && (
|
||||||
<div className="livestream-list--view-more">
|
<div className="livestream-list--view-more">
|
||||||
<Button
|
<Button
|
||||||
label={__('Show more upcoming livestreams')}
|
label={__('Show more upcoming livestreams')}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { SITE_NAME, SIMPLE_SITE, ENABLE_NO_SOURCE_CLAIMS, SHOW_ADS } from 'config';
|
import { SITE_NAME, SIMPLE_SITE, ENABLE_NO_SOURCE_CLAIMS, SHOW_ADS } from 'config';
|
||||||
import Ads, { injectAd } from 'web/component/ads';
|
import Ads, { injectAd } from 'web/component/ads';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ClaimTilesDiscover from 'component/claimTilesDiscover';
|
import ClaimTilesDiscover from 'component/claimTilesDiscover';
|
||||||
|
@ -65,6 +65,24 @@ function HomePage(props: Props) {
|
||||||
showNsfw
|
showNsfw
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type SectionHeaderProps = {
|
||||||
|
title: string,
|
||||||
|
navigate?: string,
|
||||||
|
icon?: string,
|
||||||
|
help?: string,
|
||||||
|
};
|
||||||
|
const SectionHeader = ({ title, navigate = '/', icon = '', help }: SectionHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<h1 className="claim-grid__header">
|
||||||
|
<Button navigate={navigate} button="link">
|
||||||
|
<Icon className="claim-grid__header-icon" sectionIcon icon={icon} size={20} />
|
||||||
|
<span className="claim-grid__title">{title}</span>
|
||||||
|
{help}
|
||||||
|
</Button>
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function getRowElements(title, route, link, icon, help, options, index, pinUrls) {
|
function getRowElements(title, route, link, icon, help, options, index, pinUrls) {
|
||||||
const tilePlaceholder = (
|
const tilePlaceholder = (
|
||||||
<ul className="claim-grid">
|
<ul className="claim-grid">
|
||||||
|
@ -88,13 +106,7 @@ function HomePage(props: Props) {
|
||||||
<div key={title} className="claim-grid__wrapper">
|
<div key={title} className="claim-grid__wrapper">
|
||||||
{/* category header */}
|
{/* category header */}
|
||||||
{index !== 0 && title && typeof title === 'string' && (
|
{index !== 0 && title && typeof title === 'string' && (
|
||||||
<h1 className="claim-grid__header">
|
<SectionHeader title={__(title)} navigate={route || link} icon={icon} help={help} />
|
||||||
<Button navigate={route || link} button="link">
|
|
||||||
{icon && <Icon className="claim-grid__header-icon" sectionIcon icon={icon} size={20} />}
|
|
||||||
<span className="claim-grid__title">{__(title)}</span>
|
|
||||||
{help}
|
|
||||||
</Button>
|
|
||||||
</h1>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{index === 0 && <>{claimTiles}</>}
|
{index === 0 && <>{claimTiles}</>}
|
||||||
|
@ -128,6 +140,9 @@ function HomePage(props: Props) {
|
||||||
injectAd(shouldShowAds);
|
injectAd(shouldShowAds);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [hasScheduledStreams, setHasScheduledStreams] = useState(false);
|
||||||
|
const scheduledStreamsLoaded = (total) => setHasScheduledStreams(total > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page fullWidthPage>
|
<Page fullWidthPage>
|
||||||
{!SIMPLE_SITE && (authenticated || !IS_WEB) && !subscribedChannels.length && (
|
{!SIMPLE_SITE && (authenticated || !IS_WEB) && !subscribedChannels.length && (
|
||||||
|
@ -158,8 +173,14 @@ function HomePage(props: Props) {
|
||||||
tileLayout
|
tileLayout
|
||||||
liveUris={getLivestreamUris(activeLivestreams, channelIds)}
|
liveUris={getLivestreamUris(activeLivestreams, channelIds)}
|
||||||
limitClaimsPerChannel={2}
|
limitClaimsPerChannel={2}
|
||||||
|
onLoad={scheduledStreamsLoaded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{authenticated && hasScheduledStreams && !hideScheduledLivestreams && (
|
||||||
|
<SectionHeader title={__('Following')} navigate={`/$/${PAGES.CHANNELS_FOLLOWING}`} icon={ICONS.SUBSCRIBE} />
|
||||||
|
)}
|
||||||
|
|
||||||
{rowData.map(({ title, route, link, icon, help, pinnedUrls: pinUrls, options = {} }, index) => {
|
{rowData.map(({ title, route, link, icon, help, pinnedUrls: pinUrls, options = {} }, index) => {
|
||||||
// add pins here
|
// add pins here
|
||||||
return getRowElements(title, route, link, icon, help, options, index, pinUrls);
|
return getRowElements(title, route, link, icon, help, options, index, pinUrls);
|
||||||
|
|
|
@ -68,4 +68,5 @@
|
||||||
@import 'component/empty';
|
@import 'component/empty';
|
||||||
@import 'component/stripe-card';
|
@import 'component/stripe-card';
|
||||||
@import 'component/wallet-tip-send';
|
@import 'component/wallet-tip-send';
|
||||||
|
@import 'component/swipe-list';
|
||||||
@import 'component/utils';
|
@import 'component/utils';
|
||||||
|
|
|
@ -552,6 +552,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.claim-preview--horizontal-tile {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.claim-tile__title {
|
.claim-tile__title {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
|
@ -562,7 +568,7 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
min-height: 2rem;
|
min-height: 3.2rem;
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
|
|
13
ui/scss/component/_swipe-list.scss
Normal file
13
ui/scss/component/_swipe-list.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.swipe-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-list__item {
|
||||||
|
width: 80vw;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
flex-shrink: 0;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +24,10 @@
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-0 {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-s {
|
.mt-s {
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
@ -80,6 +83,9 @@
|
||||||
.md\:mt-0 {
|
.md\:mt-0 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
.md\:mb-xl {
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
.md\:h-12 {
|
.md\:h-12 {
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue