Skip to content

Commit

Permalink
Merge pull request #15755 from brave/bn-carousels
Browse files Browse the repository at this point in the history
[Brave News]: Update Discover section to match new design spec.
  • Loading branch information
petemill committed Nov 7, 2022
1 parent dba082a commit 5e89cd8
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 129 deletions.
3 changes: 2 additions & 1 deletion browser/ui/webui/brave_webui_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,13 @@ void CustomizeWebUIHTMLSource(content::WebUI* web_ui,
{ "ad", IDS_BRAVE_TODAY_DISPLAY_AD_LABEL },

{ "braveNewsBackToDashboard", IDS_BRAVE_NEWS_BACK_TO_DASHBOARD },
{ "braveNewsBackButton", IDS_BRAVE_NEWS_BACK_BUTTON },
{ "braveNewsDisabledPlaceholderHeader", IDS_BRAVE_NEWS_DISABLED_PLACEHOLDER_HEADER }, // NOLINT
{ "braveNewsDisabledPlaceholderSubtitle", IDS_BRAVE_NEWS_DISABLED_PLACEHOLDER_SUBTITLE }, // NOLINT
{ "braveNewsDisabledPlaceholderEnableButton", IDS_BRAVE_NEWS_DISABLED_PLACEHOLDER_ENABLE_BUTTON }, // NOLINT
{ "braveNewsSearchPlaceholderLabel", IDS_BRAVE_NEWS_SEARCH_PLACEHOLDER_LABEL}, // NOLINT
{ "braveNewsChannelsHeader", IDS_BRAVE_NEWS_BROWSE_CHANNELS_HEADER}, // NOLINT
{ "braveNewsShowMoreButton", IDS_BRAVE_NEWS_SHOW_MORE_BUTTON},
{ "braveNewsViewAllButton", IDS_BRAVE_NEWS_VIEW_ALL_BUTTON},
{ "braveNewsAllSourcesHeader", IDS_BRAVE_NEWS_ALL_SOURCES_HEADER},
{ "braveNewsFeedsHeading", IDS_BRAVE_NEWS_FEEDS_HEADING},
{ "braveNewsFollowButtonFollowing", IDS_BRAVE_NEWS_FOLLOW_BUTTON_FOLLOWING}, // NOLINT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) 2022 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.

import * as React from 'react'
import styled, { css } from 'styled-components'
import Flex from '../../../Flex'
import FeedCard from './FeedCard'
import { ArrowRight } from './Icons'

const CARD_SIZE = 208
const CARD_SIZE_PX = `${CARD_SIZE}px`
const CARD_GAP = '16px'

const ScrollButton = styled.button<{ hidden: boolean }>`
all: unset;
position: absolute;
width: 32px;
height: 32px;
top: 32px;
background: white;
border-radius: 32px;
box-shadow: 0px 1px 4px rgba(63, 76, 99, 0.35);
display: flex;
align-items: center;
justify-content: center;
color: var(--text2);
cursor: pointer;
:hover {
box-shadow: 0px 1px 4px rgba(63, 76, 99, 0.5);
color: var(--interactive4);
}
${p => p.hidden && css`opacity: 0;`}
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
`

const ScrollButtonLeft = styled(ScrollButton)`
left: -16px;
transform: rotate(180deg);
`

const ScrollButtonRight = styled(ScrollButton)`
right: -16px;
`

const Container = styled(Flex)`
padding: 16px 0;
max-width: calc(${CARD_SIZE_PX} * 3 + ${CARD_GAP} * 2);
container-name: carousel;
container-type: inline-size;
&:not(:hover, :has(:focus-visible)) ${ScrollButton} {
opacity: 0;
}
`

const Header = styled.div`
width: 100%;
font-weight: 600;
font-size: 16px;
margin: 8px 0;
`

const Subtitle = styled.span`
font-size: 12px;
`

const CarouselContainer = styled.div`
position: relative;
`

const ItemsContainer = styled(Flex)`
margin: 8px 0;
overflow-x: auto;
scroll-snap-type: x mandatory;
&::-webkit-scrollbar {
display: none;
width: 0;
}
`

const FeedCardContainer = styled.div`
min-width: calc((100cqi - ${CARD_GAP} * 2) / 3);
max-width: ${CARD_SIZE_PX};
scroll-snap-align: start;
`

interface Props {
title: string | JSX.Element
subtitle?: React.ReactNode
publisherIds: string[]
}

export default function Carousel (props: Props) {
const scrollContainerRef = React.useRef<HTMLDivElement>()
const [availableDirections, setAvailableDirections] = React.useState<'none' | 'left' | 'right' | 'both'>('right')
const updateAvailableDirections = React.useCallback(() => {
if (!scrollContainerRef.current) return

const end = scrollContainerRef.current.scrollWidth - scrollContainerRef.current.clientWidth
const scrollPos = scrollContainerRef.current.scrollLeft
if (end <= 0) {
setAvailableDirections('none')
} else if (end > scrollPos && scrollPos > 0) {
setAvailableDirections('both')
} else if (end > scrollPos) {
setAvailableDirections('right')
} else {
setAvailableDirections('left')
}
}, [])

const scroll = React.useCallback((dir: 'left' | 'right') => {
if (!scrollContainerRef.current) return

scrollContainerRef.current.scrollBy({
behavior: 'smooth',
left: CARD_SIZE * (dir === 'left' ? -1 : 1)
})
}, [])

if (!props.publisherIds.length) {
return null
}

return (
<Container direction='column'>
<Flex direction='row' gap={8} align='center'>
<Header>{props.title}</Header>
</Flex>
{props.subtitle && <Subtitle>
{props.subtitle}
</Subtitle>}
<CarouselContainer>
<ItemsContainer direction='row' gap={CARD_GAP} ref={scrollContainerRef as any} onScroll={updateAvailableDirections}>
{props.publisherIds.map(p => <FeedCardContainer key={p}>
<FeedCard publisherId={p}/>
</FeedCardContainer>)}
</ItemsContainer>
<ScrollButtonLeft onClick={() => scroll('left')} hidden={availableDirections === 'right' || availableDirections === 'none'}>
{ArrowRight}
</ScrollButtonLeft>
<ScrollButtonRight onClick={() => scroll('right')} hidden={availableDirections === 'left' || availableDirections === 'none'}>
{ArrowRight}
</ScrollButtonRight>
</CarouselContainer>
</Container>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { useNewTabPref } from '../../../../hooks/usePref'
import { useBraveNews } from './Context'
import { getLocale } from '$web-common/locale'
import { formatMessage } from '../../../../../brave_rewards/resources/shared/lib/locale_context'
import { SuggestionsPage } from './Suggestions'
import { PopularPage } from './Popular'

const Grid = styled.div`
width: 100%;
Expand Down Expand Up @@ -104,11 +106,15 @@ const Content = styled.div`

export default function Configure () {
const [enabled, setEnabled] = useNewTabPref('isBraveTodayOptedIn')
const { setCustomizePage } = useBraveNews()
const { setCustomizePage, customizePage } = useBraveNews()

let content: JSX.Element
if (!enabled) {
content = <DisabledPlaceholder enableBraveNews={() => setEnabled(true)} />
} else if (customizePage === 'suggestions') {
content = <SuggestionsPage/>
} else if (customizePage === 'popular') {
content = <PopularPage />
} else {
content = <Discover />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Modal from './Modal'
// Leave possibility for more pages open.
type NewsPage = null
| 'news'
| 'suggestions'
| 'popular'

interface BraveNewsContext {
customizePage: NewsPage
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2022 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.

import styled from 'styled-components'

const CustomizeLink = styled.button`
all: unset;
font-weight: 600;
font-size: 12px;
line-height: 18px;
color: var(--interactive5);
cursor: pointer;
`
export default CustomizeLink
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2022 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.

import * as React from 'react'
import styled from 'styled-components'
import { getLocale } from '../../../../../common/locale'
import Button from '../../../../../web-components/button'
import Flex from '../../../Flex'
import { useBraveNews } from './Context'
import { BackArrow } from './Icons'

const BackButtonContainer = styled.div`
all: unset;
flex: 1;
&> button {
--inner-border-size: 0;
--outer-border-size: 0;
padding: 0;
&:hover {
--inner-border-size: 0;
--outer-border-size: 0;
}
}
`

const Header = styled.span`
font-weight: 500;
font-size: 16px;
color: var(--text01);
flex: 5;
text-align: center;
`

const Spacer = styled.div`flex: 1;`

export default function CustomizePage (props: {
title: string
children: React.ReactNode
}) {
const { setCustomizePage } = useBraveNews()
return <Flex direction="column">
<Flex align="center">
<BackButtonContainer>
<Button onClick={() => setCustomizePage('news')}>
{BackArrow}
{getLocale('braveNewsBackButton')}
</Button>
</BackButtonContainer>
<Header>{props.title}</Header>
<Spacer />
</Flex>
{props.children}
</Flex>
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import ChannelCard from './ChannelCard'
import DiscoverSection from './DiscoverSection'
import FeedCard, { DirectFeedCard } from './FeedCard'
import useSearch from './useSearch'
import Suggestions from './Suggestions'
import Popular from './Popular'
import { SuggestionsCarousel } from './Suggestions'
import { PopularCarousel } from './Popular'

const Header = styled.span`
font-size: 24px;
Expand All @@ -31,16 +31,6 @@ const SearchInput = styled(TextInput)`
--focus-border: #737ADE;
`

const LoadMoreButtonContainer = styled.div`
display: flex;
flex-direction: column;
align-items: stretch;
grid-column: 2;
`

// The default number of category cards to show.
const DEFAULT_NUM_CATEGORIES = 3

export default function Discover () {
const [query, setQuery] = useState('')

Expand All @@ -55,40 +45,23 @@ export default function Discover () {
}

function Home () {
const [showingAllCategories, setShowingAllCategories] = React.useState(false)
const channels = useChannels()
const { filteredPublisherIds, updateSuggestedPublisherIds } = useBraveNews()
const { updateSuggestedPublisherIds } = useBraveNews()

const visibleChannelIds = React.useMemo(() => channels
// If we're showing all channels, there's no end to the slice.
// Otherwise, just show the default number.
.slice(0, showingAllCategories
? undefined
: DEFAULT_NUM_CATEGORIES)
.map(c => c.channelName),
[channels, showingAllCategories])
const channelNames = React.useMemo(() => channels.map(c => c.channelName),
[channels])

// When we mount this component, update the suggested publisher ids.
React.useEffect(() => { updateSuggestedPublisherIds() }, [])

return (
<>
<Suggestions/>
<Popular/>
<PopularCarousel />
<SuggestionsCarousel />
<DiscoverSection name={getLocale('braveNewsChannelsHeader')}>
{visibleChannelIds.map(channelName =>
{channelNames.map(channelName =>
<ChannelCard key={channelName} channelName={channelName} />
)}
{!showingAllCategories && <LoadMoreButtonContainer>
<Button onClick={() => setShowingAllCategories(true)}>
{getLocale('braveNewsShowMoreButton')}
</Button>
</LoadMoreButtonContainer>}
</DiscoverSection>
<DiscoverSection name={getLocale('braveNewsAllSourcesHeader')}>
{filteredPublisherIds.map(publisherId =>
<FeedCard key={publisherId} publisherId={publisherId} />
)}
</DiscoverSection>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import styled from 'styled-components'
import Flex from '../../../Flex'

interface Props {
name: string
name?: string
subtitle?: React.ReactNode
children?: React.ReactNode
}
Expand Down Expand Up @@ -36,9 +36,9 @@ const ItemsContainer = styled.div`

export default function DiscoverSection (props: Props) {
return <Container direction='column'>
<Flex direction='row' gap={8} align='center'>
{props.name && <Flex direction='row' gap={8} align='center'>
<Header>{props.name}</Header>
</Flex>
</Flex>}
{props.subtitle && <Subtitle>
{props.subtitle}
</Subtitle>}
Expand Down
Loading

0 comments on commit 5e89cd8

Please sign in to comment.