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:
Dan Peterson 2022-01-06 15:13:26 -06:00 committed by GitHub
parent 58bdcbd1ed
commit a89cb17ce4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 92 additions and 17 deletions

View file

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

View file

@ -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 &&

View file

@ -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 ? (

View file

@ -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>

View file

@ -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')}

View file

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

View file

@ -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';

View file

@ -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;

View 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;
}

View file

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