From 1a7c5210b1ed0d93f09abbcd3fdedd8da3df51e4 Mon Sep 17 00:00:00 2001 From: Raphaelle Cantin Date: Mon, 2 Dec 2024 11:44:04 +0000 Subject: [PATCH 1/2] Add basic recommended stories container under toggle --- .../components/RecommendedStories/index.tsx | 209 ++++++++++++++++++ common/views/slices/Text/index.tsx | 93 +++++++- content/webapp/components/Body/Body.tsx | 119 ++++++++-- content/webapp/pages/stories/[articleId].tsx | 3 + 4 files changed, 401 insertions(+), 23 deletions(-) create mode 100644 common/views/components/RecommendedStories/index.tsx diff --git a/common/views/components/RecommendedStories/index.tsx b/common/views/components/RecommendedStories/index.tsx new file mode 100644 index 0000000000..39e72a157b --- /dev/null +++ b/common/views/components/RecommendedStories/index.tsx @@ -0,0 +1,209 @@ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +import { font } from '@weco/common/utils/classnames'; +import { Container } from '@weco/common/views/components/styled/Container'; +import Space from '@weco/common/views/components/styled/Space'; +import { WobblyBottom } from '@weco/common/views/components/WobblyEdge'; + +const RecommendedSection = styled(Space).attrs({ + $v: { size: 'l', properties: ['margin-bottom', 'margin-top'] }, +})` + background-color: ${props => props.theme.color('white')}; +`; + +const StoryPromoContainer = styled(Container)` + padding: 0 24px; + max-width: none; + width: auto; + overflow: auto; + margin: 0 auto; + padding-bottom: ${props => props.theme.containerPadding.large}px; + + ${props => + props.theme.media( + 'medium', + 'max-width' + )(` + padding-bottom: ${props.theme.containerPadding.small}px; + `)} + + &::-webkit-scrollbar { + background: ${props => props.theme.color('neutral.200')}; + height: 18px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 0; + border-style: solid; + border-width: 0 ${props => props.theme.containerPadding.small}px 12px; + background: ${props => props.theme.color('neutral.400')}; + border-color: ${props => props.theme.color('neutral.200')}; + } + + -webkit-overflow-scrolling: touch; +`; + +const StoriesContainer = styled.ul` + list-style-type: none; + padding: 0; + margin: 0; + flex-basis: 30%; + max-width: 30%; + + margin-left: 0; + display: flex; + flex-wrap: nowrap; +`; + +const StoryLink = styled.a` + text-decoration: none; + + &:hover { + text-decoration: underline; + } +`; + +const StoryWrapper = styled.li` + flex: 1; + min-width: 33vw; + max-width: 345px; + padding-left: 0; + padding-right: ${props => props.theme.gutter.small}px; + text-decoration: none; + + &:last-child { + padding-bottom: 0; + } + + ${props => props.theme.media('large', 'max-width')` + min-width: 40vw; + max-width: 300px; + `} + + ${props => props.theme.media('medium', 'max-width')` + min-width: 80vw; + `} +`; + +const Story = styled.div` + display: flex; + align-items: center; + background-color: ${props => props.theme.color('warmNeutral.300')}; + border-radius: 6px; + overflow: hidden; + + img { + max-width: 96px; + max-height: 96px; + margin-right: 8px; + } + + h4 { + margin: 0 8px 0 0; + } +`; + +type RecStories = { + title: string; + path: string; + imageUrl: string; +}; + +const storiesArray: RecStories[] = [ + { + title: 'Finding Audrey Amiss', + path: '/series/finding-audrey-amiss', + imageUrl: + 'https://images.prismic.io/wellcomecollection/ff095f17-11fc-4ec7-af22-6ed73dd75fb1_4k-sRGB_pp_ami_d_145_b32878229_0014.jpg?auto=compress,format&rect=755,0,1362,1362&w=200&h=200', + }, + { + title: 'Medieval mobility aids', + path: '/stories/medieval-mobility-aids', + imageUrl: + 'https://images.prismic.io/wellcomecollection/e86c3ba9-9bc6-4217-a72d-95d12244b5b1_EP001691_0001.jpg?auto=compress,format&rect=0,0,1994,1994&w=200&h=200', + }, + { + title: '‘My Hair Is Not…’', + path: '/stories/-my-hair-is-not--', + imageUrl: + 'https://images.prismic.io/wellcomecollection/bba64522-b65a-4c7e-8878-a8e8b5b51271_Headline.jpg?auto=compress,format&rect=827,0,2125,2125&w=200&h=200', + }, + { + title: 'A brief history of tattoos', + path: '/stories/a-brief-history-of-tattoos', + imageUrl: + 'https://images.prismic.io/wellcomecollection%2Fca88bb42-adf1-40fa-bfd3-dc3d4e87a53d_te+manawa_+an+arawa+warrior.+watercolour+by+h.g.+robley+crop+169.png?auto=format%2Ccompress&rect=166%2C0%2C458%2C458&w=200&h=200', + }, + { + title: '‘Yes I am’ – voices of autistic women from minoritised communities', + path: '/stories/yes-i-am--voices-of-autistic-women-from-minoritised-communities', + imageUrl: + 'https://images.prismic.io/wellcomecollection/ZxKdYIF3NbkBXugW_YesIAm-1.jpg?auto=format%2Ccompress&rect=99%2C93%2C3308%2C3308&w=200&h=200', + }, + { + title: 'The breastmilk market', + path: '/series/the-breastmilk-market', + imageUrl: + 'https://images.prismic.io/wellcomecollection/bf81194b-372e-4803-aeb5-ea4e8797725c_SDP_230511_VIckyScott_0001.jpg?auto=compress,format&rect=820,0,2250,2250&w=200&h=200', + }, +]; + +const getStories = (currentPath: string) => { + // Removes story if on said story page + const filteredStories = storiesArray.filter( + story => story.path !== currentPath + ); + + // Randomiser taken from https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array + for (let i = filteredStories.length - 1; i >= 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [filteredStories[i], filteredStories[j]] = [ + filteredStories[j], + filteredStories[i], + ]; + } + + return filteredStories; +}; + +const RecommendedStories = () => { + const [stories, setStories] = useState(); + + useEffect(() => { + if (!stories) { + setStories(getStories(window.location.pathname)); + } + }, []); + + if (!stories) return null; + + return ( + +

+ Recommended articles +

+ + + + {stories.map(story => { + return ( + + + + {story.title} +

{story.title}

+
+
+
+ ); + })} +
+
+ + +
+ ); +}; + +export default RecommendedStories; diff --git a/common/views/slices/Text/index.tsx b/common/views/slices/Text/index.tsx index 7b751a7b3f..807be3d78a 100644 --- a/common/views/slices/Text/index.tsx +++ b/common/views/slices/Text/index.tsx @@ -3,8 +3,10 @@ import { SliceComponentProps } from '@prismicio/react'; import { FunctionComponent } from 'react'; import { TextSlice as RawTextSlice } from '@weco/common/prismicio-types'; +import { useToggles } from '@weco/common/server-data/Context'; import { classNames } from '@weco/common/utils/classnames'; import PrismicHtmlBlock from '@weco/common/views/components/PrismicHtmlBlock/PrismicHtmlBlock'; +import RecommendedStories from '@weco/common/views/components/RecommendedStories'; import SpacingComponent from '@weco/common/views/components/styled/SpacingComponent'; import { defaultContext, @@ -18,18 +20,103 @@ import { export type TextProps = SliceComponentProps; +const RecommendedStoriesTest = ({ + slice, + context, + index, + shouldBeDroppedCap, + options, +}) => { + return ( + <> + + +
+ {shouldBeDroppedCap ? ( + <> + + + + ) : ( + + )} +
+
+
+ + + + + +
+ +
+
+
+ + ); +}; + const Text: FunctionComponent = ({ slice, context, index }) => { const options = { ...defaultContext, ...context }; - return ( + const { recommendedStories } = useToggles(); + const shouldBeDroppedCap = + options.firstTextSliceIndex === slice.id && options.isDropCapped; + + return recommendedStories && context.fifthParagraphIndex !== undefined ? ( + + ) : (
- {options.firstTextSliceIndex === index && options.isDropCapped ? ( + {shouldBeDroppedCap ? ( <> (props => ({ export type SliceZoneContext = { minWidth: 8 | 10 | 12; - firstTextSliceIndex: number; + firstTextSliceIndex: string; isVisualStory: boolean; comicPreviousNext?: ComicPreviousNextProps; isShortFilm: boolean; @@ -131,11 +132,12 @@ export type SliceZoneContext = { isLanding: boolean; isDropCapped: boolean; contentType?: 'short-film' | 'visual-story' | 'standalone-image-gallery'; + fifthParagraphIndex?: number; }; export const defaultContext: SliceZoneContext = { minWidth: 8, - firstTextSliceIndex: 1, + firstTextSliceIndex: '', isVisualStory: false, comicPreviousNext: undefined, isShortFilm: false, @@ -145,6 +147,63 @@ export const defaultContext: SliceZoneContext = { contentType: undefined, }; +const TransformedSliceZone = ({ slices, components, context }) => { + let paragraphCount = 0; + let hasFoundFifthParagraph = false; + let index; + + return slices.map(slice => { + let isTheSliceWFifthParagraph = false; + + if (slice.slice_type === 'text') { + // Go through all Text slices + if ( + !hasFoundFifthParagraph && + paragraphCount <= 5 && + Array.isArray(slice.primary?.text) + ) { + // Find all paragraphs within each Text slice until we find the fifth one. + slice.primary?.text.forEach((t, textIndex) => { + if ( + !hasFoundFifthParagraph && + paragraphCount <= 5 && + t.type === 'paragraph' + ) { + if (paragraphCount < 5) { + paragraphCount++; + } else { + index = textIndex; + hasFoundFifthParagraph = true; + isTheSliceWFifthParagraph = true; + } + } + }); + } + + return ( + + ); + } + + return ( + + ); + }); +}; + const Body: FunctionComponent = ({ untransformedBody, introText, @@ -158,14 +217,15 @@ const Body: FunctionComponent = ({ staticContent = null, comicPreviousNext, contentType, + hasRecommendations, }: Props) => { const filteredUntransformedBody = untransformedBody.filter( slice => slice.slice_type !== 'standfirst' ); - const firstTextSliceIndex = filteredUntransformedBody - .map(slice => slice.slice_type) - .indexOf('text'); + const firstTextSliceIndex = + filteredUntransformedBody.find(slice => slice.slice_type === 'text')?.id || + ''; const sections: RawContentListSlice[] = untransformedBody.filter(isContentList); @@ -333,21 +393,40 @@ const Body: FunctionComponent = ({ {isLanding && } - + {hasRecommendations ? ( + + ) : ( + + )} ); }; diff --git a/content/webapp/pages/stories/[articleId].tsx b/content/webapp/pages/stories/[articleId].tsx index 69d799fced..f05485c01d 100644 --- a/content/webapp/pages/stories/[articleId].tsx +++ b/content/webapp/pages/stories/[articleId].tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { bodySquabblesSeries } from '@weco/common/data/hardcoded-ids'; import { getServerData } from '@weco/common/server-data'; +import { useToggles } from '@weco/common/server-data/Context'; import { AppErrorProps } from '@weco/common/services/app'; import { GaDimensions } from '@weco/common/services/app/analytics-scripts'; import { Pageview } from '@weco/common/services/conversion/track'; @@ -165,6 +166,7 @@ const HTMLDateWrapper = styled.span.attrs({ className: font('intr', 6) })` const ArticlePage: FunctionComponent = ({ article, jsonLd }) => { const [listOfSeries, setListOfSeries] = useState(); + const { recommendedStories } = useToggles(); useEffect(() => { async function setSeries() { @@ -360,6 +362,7 @@ const ArticlePage: FunctionComponent = ({ article, jsonLd }) => { ? 'standalone-image-gallery' : undefined } + hasRecommendations={recommendedStories} /> } RelatedContent={Siblings} From 7726b2b63fe32edcbfe9337c182669d578fe6e57 Mon Sep 17 00:00:00 2001 From: Raphaelle Cantin Date: Mon, 2 Dec 2024 12:03:41 +0000 Subject: [PATCH 2/2] Move the scrollbar and change title --- .../views/components/RecommendedStories/index.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/common/views/components/RecommendedStories/index.tsx b/common/views/components/RecommendedStories/index.tsx index 39e72a157b..c89801ef6b 100644 --- a/common/views/components/RecommendedStories/index.tsx +++ b/common/views/components/RecommendedStories/index.tsx @@ -13,23 +13,22 @@ const RecommendedSection = styled(Space).attrs({ `; const StoryPromoContainer = styled(Container)` - padding: 0 24px; + padding: 0 24px ${props => props.theme.containerPadding.small}px; max-width: none; width: auto; overflow: auto; margin: 0 auto; - padding-bottom: ${props => props.theme.containerPadding.large}px; + margin-bottom: ${props => props.theme.containerPadding.medium}px; ${props => props.theme.media( 'medium', 'max-width' )(` - padding-bottom: ${props.theme.containerPadding.small}px; + margin-bottom: ${props.theme.containerPadding.small}px; `)} &::-webkit-scrollbar { - background: ${props => props.theme.color('neutral.200')}; height: 18px; } @@ -37,8 +36,8 @@ const StoryPromoContainer = styled(Container)` border-radius: 0; border-style: solid; border-width: 0 ${props => props.theme.containerPadding.small}px 12px; - background: ${props => props.theme.color('neutral.400')}; - border-color: ${props => props.theme.color('neutral.200')}; + background: ${props => props.theme.color('neutral.300')}; + border-color: ${props => props.theme.color('white')}; } -webkit-overflow-scrolling: touch; @@ -181,7 +180,7 @@ const RecommendedStories = () => { return (

- Recommended articles + Popular stories