From fe18feee0602318963cc565418f3240023f56194 Mon Sep 17 00:00:00 2001 From: andreashelms Date: Thu, 11 Jan 2024 16:48:12 +0100 Subject: [PATCH] feat(story): create mixed content gallery component --- src/scripts/actions/fetch-story.ts | 6 +- .../stories/splash-screen/splash-screen.tsx | 3 +- .../stories/story-content/story-content.tsx | 3 - .../story-gallery-image.tsx | 76 ----------------- .../story-gallery-item.module.styl} | 20 ++--- .../story-gallery-item/story-gallery-item.tsx | 44 ++++++++++ .../stories/story-gallery/story-gallery.tsx | 56 ++++++------- .../story-globe/story-globe.module.styl | 41 ++++++++++ .../stories/story-globe/story-globe.tsx | 45 +++++++++++ .../story-image/story-image.module.styl | 4 + .../stories/story-image/story-image.tsx | 39 +++++++++ .../stories/story-progress/story-progress.tsx | 6 +- .../story-video/story-video.module.styl | 1 + .../stories/story-video/story-video.tsx | 8 +- .../stories/story/story.module.styl | 41 +--------- .../components/stories/story/story.tsx | 81 +++++++++---------- .../{use-slide.ts => use-story-globe.ts} | 13 +-- src/scripts/hooks/use-story-navigation.ts | 30 +++---- src/scripts/libs/convert-legacy-story.ts | 80 ++++++++++++++++++ src/scripts/reducers/story/selected.ts | 4 +- src/scripts/types/gallery-item.ts | 49 +++++++++++ .../types/{story.d.ts => legacy-story.ts} | 13 +-- src/scripts/types/slide-type.ts | 1 + src/scripts/types/story.ts | 13 +++ storage/stories/debug/debug-en.json | 6 ++ 25 files changed, 439 insertions(+), 244 deletions(-) delete mode 100644 src/scripts/components/stories/story-gallery-image/story-gallery-image.tsx rename src/scripts/components/stories/{story-gallery-image/story-gallery-image.module.styl => story-gallery-item/story-gallery-item.module.styl} (91%) create mode 100644 src/scripts/components/stories/story-gallery-item/story-gallery-item.tsx create mode 100644 src/scripts/components/stories/story-globe/story-globe.module.styl create mode 100644 src/scripts/components/stories/story-globe/story-globe.tsx create mode 100644 src/scripts/components/stories/story-image/story-image.module.styl create mode 100644 src/scripts/components/stories/story-image/story-image.tsx rename src/scripts/hooks/{use-slide.ts => use-story-globe.ts} (79%) create mode 100644 src/scripts/libs/convert-legacy-story.ts create mode 100644 src/scripts/types/gallery-item.ts rename src/scripts/types/{story.d.ts => legacy-story.ts} (85%) create mode 100644 src/scripts/types/story.ts diff --git a/src/scripts/actions/fetch-story.ts b/src/scripts/actions/fetch-story.ts index 15f4bb046..878b1b992 100644 --- a/src/scripts/actions/fetch-story.ts +++ b/src/scripts/actions/fetch-story.ts @@ -4,7 +4,7 @@ import fetchStoryApi from '../api/fetch-story'; import {languageSelector} from '../selectors/language'; import {State} from '../reducers/index'; -import {Story} from '../types/story'; +import {LegacyStory} from '../types/legacy-story'; export const FETCH_STORY_SUCCESS = 'FETCH_STORY_SUCCESS'; export const FETCH_STORY_ERROR = 'FETCH_STORY_ERROR'; @@ -13,7 +13,7 @@ interface FetchStorySuccessAction { type: typeof FETCH_STORY_SUCCESS; id: string; language: string; - story: Story; + story: LegacyStory; } interface FetchStoryErrorAction { @@ -27,7 +27,7 @@ export type FetchStoryActions = FetchStorySuccessAction | FetchStoryErrorAction; export function fetchStorySuccessAction( storyId: string, language: string, - story: Story + story: LegacyStory ) { return { type: FETCH_STORY_SUCCESS, diff --git a/src/scripts/components/stories/splash-screen/splash-screen.tsx b/src/scripts/components/stories/splash-screen/splash-screen.tsx index 09021ab0d..a289ae9d1 100644 --- a/src/scripts/components/stories/splash-screen/splash-screen.tsx +++ b/src/scripts/components/stories/splash-screen/splash-screen.tsx @@ -16,7 +16,8 @@ interface Props { } const SplashScreen: FunctionComponent = ({storyId, mode, slide}) => { - const imageUrl = slide.images && getStoryAssetUrl(storyId, slide.images[0]); + const imageUrl = + slide.splashImage && getStoryAssetUrl(storyId, slide.splashImage); const contentClasses = cx( styles.content, mode !== StoryMode.Stories && styles.presentationContent diff --git a/src/scripts/components/stories/story-content/story-content.tsx b/src/scripts/components/stories/story-content/story-content.tsx index 6a3379b30..5b479f5d8 100644 --- a/src/scripts/components/stories/story-content/story-content.tsx +++ b/src/scripts/components/stories/story-content/story-content.tsx @@ -3,7 +3,6 @@ import ReactMarkdown from 'react-markdown'; import cx from 'classnames'; import {getStoryAssetUrl} from '../../../libs/get-story-asset-urls'; -import {useSlide} from '../../../hooks/use-slide'; import config from '../../../config/main'; import {StoryMode} from '../../../types/story-mode'; @@ -20,8 +19,6 @@ interface Props { const StoryContent: FunctionComponent = ({mode, slide, storyId}) => { const storyText = mode === StoryMode.Stories ? slide.text : slide.shortText; - useSlide(slide); - const contentClasses = cx( styles.content, mode !== StoryMode.Stories && styles.shortTextContent diff --git a/src/scripts/components/stories/story-gallery-image/story-gallery-image.tsx b/src/scripts/components/stories/story-gallery-image/story-gallery-image.tsx deleted file mode 100644 index 7ff680397..000000000 --- a/src/scripts/components/stories/story-gallery-image/story-gallery-image.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, {FunctionComponent} from 'react'; -import cx from 'classnames'; - -import {getStoryAssetUrl} from '../../../libs/get-story-asset-urls'; -import Caption from '../caption/caption'; - -import {ImageFit} from '../../../types/image-fit'; - -import styles from './story-gallery-image.module.styl'; - -interface Props { - images: string[]; - imageCaptions?: string[]; - imageFits?: ImageFit[]; - storyId: string; - currentIndex: number; - showLightbox: boolean; -} - -const StoryGalleryImage: FunctionComponent = ({ - images, - imageCaptions, - storyId, - imageFits, - currentIndex, - showLightbox -}) => { - const containerWidth = images.length * 100; - const imageWidth = 100 / images.length; - const imgClasses = cx( - styles.slider, - showLightbox && styles.lightboxStoryGallery, - images.length > 1 && styles.transition - ); - - return ( -
- {images.map((image, index) => { - const imageCaption = imageCaptions?.find((_, i) => i === index); - const imageFit = imageFits?.find((_, i) => i === index); - const imageUrl = getStoryAssetUrl(storyId, image); - - return ( -
-
- - {imageCaption && ( - - )} -
-
- ); - })} -
- ); -}; - -export default StoryGalleryImage; diff --git a/src/scripts/components/stories/story-gallery-image/story-gallery-image.module.styl b/src/scripts/components/stories/story-gallery-item/story-gallery-item.module.styl similarity index 91% rename from src/scripts/components/stories/story-gallery-image/story-gallery-image.module.styl rename to src/scripts/components/stories/story-gallery-item/story-gallery-item.module.styl index 46b73115e..d31a75bb5 100644 --- a/src/scripts/components/stories/story-gallery-image/story-gallery-image.module.styl +++ b/src/scripts/components/stories/story-gallery-item/story-gallery-item.module.styl @@ -5,14 +5,14 @@ align-items: center height: 100% - .sliderImage + .sliderItem display: flex justify-content: center align-items: center width: 100% height: 100% - .imageContainer + .itemContainer display: flex flex-direction: column justify-content: center @@ -28,13 +28,7 @@ display: flex height: 100% -.transition - transition: transform ease-out 0.5s - -.sliderImage - height: 100% - -.imageContainer +.itemContainer position: relative display: flex flex-direction: column @@ -46,9 +40,15 @@ width: 100% height: 0 +.transition + transition: transform ease-out 0.5s + +.sliderItem + height: 100% + @media screen and (max-width: 480px) .lightboxStoryGallery - .imageContainer + .itemContainer max-width: 100% max-height: 100% background-color: $plainBlack diff --git a/src/scripts/components/stories/story-gallery-item/story-gallery-item.tsx b/src/scripts/components/stories/story-gallery-item/story-gallery-item.tsx new file mode 100644 index 000000000..f57597a28 --- /dev/null +++ b/src/scripts/components/stories/story-gallery-item/story-gallery-item.tsx @@ -0,0 +1,44 @@ +import React, {FunctionComponent} from 'react'; +import cx from 'classnames'; + +import styles from './story-gallery-item.module.styl'; + +interface Props { + children: React.ReactNode[]; + currentIndex: number; + showLightbox: boolean; +} + +const StoryGalleryItem: FunctionComponent = ({ + children, + currentIndex, + showLightbox +}) => { + const containerWidth = children.length * 100; + const itemWidth = 100 / children.length; + const imgClasses = cx( + styles.slider, + showLightbox && styles.lightboxStoryGallery, + children.length > 1 && styles.transition + ); + + return ( +
+ {children.map((child, index) => ( +
+
{child}
+
+ ))} +
+ ); +}; + +export default StoryGalleryItem; diff --git a/src/scripts/components/stories/story-gallery/story-gallery.tsx b/src/scripts/components/stories/story-gallery/story-gallery.tsx index f603a60d4..fe1db7bd9 100644 --- a/src/scripts/components/stories/story-gallery/story-gallery.tsx +++ b/src/scripts/components/stories/story-gallery/story-gallery.tsx @@ -12,38 +12,29 @@ import {FullscreenIcon} from '../../main/icons/fullscreen-icon'; import {useInterval} from '../../../hooks/use-interval'; import config from '../../../config/main'; import {CloseIcon} from '../../main/icons/close-icon'; -import StoryGalleryImage from '../story-gallery-image/story-gallery-image'; +import StoryGalleryItem from '../story-gallery-item/story-gallery-item'; import StoryProgress from '../story-progress/story-progress'; import {StoryMode} from '../../../types/story-mode'; -import {ImageFit} from '../../../types/image-fit'; import styles from './story-gallery.module.styl'; interface Props { - images: string[]; - imageCaptions?: string[]; - imageFits?: ImageFit[]; storyId: string; mode: StoryMode | null; + children: React.ReactElement[]; } -const StoryGallery: FunctionComponent = ({ - images, - imageCaptions, - storyId, - imageFits, - mode -}) => { +const StoryGallery: FunctionComponent = ({mode, children}) => { const [currentIndex, setCurrentIndex] = useState(0); const [showLightbox, setShowLightbox] = useState(false); const showPrevButton = currentIndex > 0; - const showNextButton = currentIndex < images.length - 1; + const showNextButton = currentIndex < children.length - 1; const delay = mode === StoryMode.Showcase ? config.delay : null; useInterval(() => { if (mode === StoryMode.Showcase) { - if (currentIndex >= images.length - 1) { + if (currentIndex >= children.length - 1) { return; } setCurrentIndex(currentIndex + 1); @@ -58,7 +49,7 @@ const StoryGallery: FunctionComponent = ({ }; const onNextClick = () => { - if (currentIndex >= images.length - 1) { + if (currentIndex >= children.length - 1) { return; } setCurrentIndex(currentIndex + 1); @@ -100,11 +91,13 @@ const StoryGallery: FunctionComponent = ({ return (
- + {children.length > 1 && ( + + )}
{!showLightbox ? (
= ({
)} - -
-
- + {children.length > 1 && ( +
+
+ +
+
+ +
-
- -
-
+ )}
); diff --git a/src/scripts/components/stories/story-globe/story-globe.module.styl b/src/scripts/components/stories/story-globe/story-globe.module.styl new file mode 100644 index 000000000..0736e62bb --- /dev/null +++ b/src/scripts/components/stories/story-globe/story-globe.module.styl @@ -0,0 +1,41 @@ +.globeContainer + position: relative + display: flex + flex-direction: column + height: 100% + padding: 0 emCalc(50px) + background-color: $plainBlack + +.layerDetails + position: absolute + right: 0 + bottom: 0 + left: 0 + padding: inherit + +.storySlider + position: unset + padding: emCalc(40px) 0 + background: none + + > div + min-width: 25% + width: 60% + +@media screen and (max-width: 1025px) + .layerDetails + height: inherit + background: none + pointer-events: none + + > * + position: relative + z-index: 1 + pointer-events: all + + .storySlider + position: absolute + right: 0 + bottom: 0 + padding: emCalc(40px) 0 emCalc(20px) + diff --git a/src/scripts/components/stories/story-globe/story-globe.tsx b/src/scripts/components/stories/story-globe/story-globe.tsx new file mode 100644 index 000000000..979dc0faa --- /dev/null +++ b/src/scripts/components/stories/story-globe/story-globe.tsx @@ -0,0 +1,45 @@ +import React, {FunctionComponent} from 'react'; +import {useSelector} from 'react-redux'; +import {useStoryGlobe} from '../../../hooks/use-story-globe'; + +import {embedElementsSelector} from '../../../selectors/embed-elements-selector'; + +import DataViewer from '../../main/data-viewer/data-viewer'; +import TimeSlider from '../../layers/time-slider/time-slider'; +import LayerDescription from '../layer-description/layer-description'; + +import {GlobeItem} from '../../../types/gallery-item'; + +import styles from './story-globe.module.styl'; + +interface Props { + globeItem: GlobeItem; +} + +const StoryGlobe: FunctionComponent = ({globeItem}) => { + // eslint-disable-next-line camelcase + const {time_slider} = useSelector(embedElementsSelector); + + useStoryGlobe(globeItem); + + return ( +
+ + {/* eslint-disable-next-line camelcase */} + {time_slider && ( +
+ + {globeItem.layerDescription && ( + + )} +
+ )} +
+ ); +}; + +export default StoryGlobe; diff --git a/src/scripts/components/stories/story-image/story-image.module.styl b/src/scripts/components/stories/story-image/story-image.module.styl new file mode 100644 index 000000000..637aac323 --- /dev/null +++ b/src/scripts/components/stories/story-image/story-image.module.styl @@ -0,0 +1,4 @@ +.photo + flex: 1 + width: 100% + height: 0 diff --git a/src/scripts/components/stories/story-image/story-image.tsx b/src/scripts/components/stories/story-image/story-image.tsx new file mode 100644 index 000000000..9759c413d --- /dev/null +++ b/src/scripts/components/stories/story-image/story-image.tsx @@ -0,0 +1,39 @@ +import React, {FunctionComponent} from 'react'; +import {getStoryAssetUrl} from '../../../libs/get-story-asset-urls'; + +import Caption from '../caption/caption'; + +import {ImageItem} from '../../../types/gallery-item'; +import {ImageFit} from '../../../types/image-fit'; + +import styles from './story-image.module.styl'; + +interface Props { + storyId: string; + imageItem: ImageItem; +} + +const StoryImage: FunctionComponent = ({storyId, imageItem}) => { + const imageUrl = getStoryAssetUrl(storyId, imageItem.image); + const {imageCaption, imageFit} = imageItem; + return ( + <> + + {imageCaption && ( + + )} + + ); +}; + +export default StoryImage; diff --git a/src/scripts/components/stories/story-progress/story-progress.tsx b/src/scripts/components/stories/story-progress/story-progress.tsx index 276a1ba40..315808550 100644 --- a/src/scripts/components/stories/story-progress/story-progress.tsx +++ b/src/scripts/components/stories/story-progress/story-progress.tsx @@ -4,13 +4,13 @@ import cx from 'classnames'; import styles from './story-progress.module.styl'; interface Props { - images: string[]; + children: React.ReactElement[]; currentIndex: number; showLightbox: boolean; } const StoryProgress: FunctionComponent = ({ - images, + children, currentIndex, showLightbox }) => { @@ -22,7 +22,7 @@ const StoryProgress: FunctionComponent = ({ return (
- {images.map((_, index) => ( + {children.map((_, index) => (
void; } const StoryVideo: FunctionComponent = ({ mode, storyId, - slide, + videoItem, onPlay }) => { - const {videoSrc, videoId, videoCaptions, videoPoster} = slide; + const {videoSrc, videoId, videoCaptions, videoPoster} = videoItem; const language = useSelector(languageSelector); const isStoryMode = mode === StoryMode.Stories; const classes = cx( diff --git a/src/scripts/components/stories/story/story.module.styl b/src/scripts/components/stories/story/story.module.styl index f2be622d4..748d04cab 100644 --- a/src/scripts/components/stories/story/story.module.styl +++ b/src/scripts/components/stories/story/story.module.styl @@ -19,51 +19,16 @@ .rightSideComponent height: 100% -.globeContainer - position: relative - display: flex - flex-direction: column +.embeddedContent + border: 0 height: 100% - background-color: $plainBlack - -.layerDetails - position: absolute - right: 0 - bottom: 0 - left: 0 - padding: inherit - -.storySlider - position: unset - padding: emCalc(40px) 0 - background: none - - > div - min-width: 25% - width: 60% + padding: emCalc(56px) .layerDescription position: unset padding-right: 0 min-height: $storyCaptionHeight -@media screen and (max-width: 1025px) - .layerDetails - height: inherit - background: none - pointer-events: none - - > * - position: relative - z-index: 1 - pointer-events: all - - .storySlider - position: absolute - right: 0 - bottom: 0 - padding: emCalc(40px) 0 emCalc(20px) - @media screen and (max-width: 480px) .main display: flex diff --git a/src/scripts/components/stories/story/story.tsx b/src/scripts/components/stories/story/story.tsx index 03c014924..75a6ceea9 100644 --- a/src/scripts/components/stories/story/story.tsx +++ b/src/scripts/components/stories/story/story.tsx @@ -4,8 +4,9 @@ import {YouTubePlayer} from 'youtube-player/dist/types'; import {VideoJsPlayer} from 'video.js'; import {useSelector} from 'react-redux'; -import DataViewer from '../../main/data-viewer/data-viewer'; import {useStoryParams} from '../../../hooks/use-story-params'; +import StoryImage from '../story-image/story-image'; +import StoryGlobe from '../story-globe/story-globe'; import StoryContent from '../story-content/story-content'; import StoryGallery from '../story-gallery/story-gallery'; import StoryFooter from '../story-footer/story-footer'; @@ -17,14 +18,12 @@ import setSelectedLayerIdsAction from '../../../actions/set-selected-layer-id'; import setGlobeTimeAction from '../../../actions/set-globe-time'; import Share from '../../main/share/share'; import SplashScreen from '../splash-screen/splash-screen'; -import LayerDescription from '../layer-description/layer-description'; -import TimeSlider from '../../layers/time-slider/time-slider'; import {embedElementsSelector} from '../../../selectors/embed-elements-selector'; -import {SlideType} from '../../../types/slide-type'; import {GlobeProjection} from '../../../types/globe-projection'; import {StoryMode} from '../../../types/story-mode'; import {Slide, Story as StoryType} from '../../../types/story'; +import {GalleryItemType} from '../../../types/gallery-item'; import {useThunkDispatch} from '../../../hooks/use-thunk-dispatch'; import styles from './story.module.styl'; @@ -37,9 +36,8 @@ const Story: FunctionComponent = () => { const {mode, slideIndex, currentStoryId, selectedStory, storyListItem} = storyParams; const storyMode = mode === StoryMode.Stories; - const isSplashScreen = - selectedStory?.slides[slideIndex].type === SlideType.Splashscreen; - const {story_header, time_slider} = useSelector(embedElementsSelector); + const isSplashScreen = Boolean(selectedStory?.slides[slideIndex].splashImage); + const {story_header} = useSelector(embedElementsSelector); // fetch story of active storyId useEffect(() => { @@ -76,49 +74,44 @@ const Story: FunctionComponent = () => { }; const getRightSideComponent = (slide: Slide, story: StoryType) => { - if (slide.type === SlideType.Image && slide.images) { + if (slide.galleryItems) { return ( { + switch (item.type) { + case GalleryItemType.Image: + return ; + case GalleryItemType.Video: + return item.videoSrc || item.videoId ? ( + + getVideoDuration(player) + } + /> + ) : ( + <> + ); + case GalleryItemType.Globe: + return ; + case GalleryItemType.Embedded: + return ( + + ); + default: + return <>; + } + })} storyId={story.id} /> ); - } else if ( - slide.type === SlideType.Video && - (slide.videoSrc || slide.videoId) - ) { - return ( - - getVideoDuration(player) - } - /> - ); } - - return ( -
- - {time_slider && ( -
- - {slide.layerDescription && ( - - )} -
- )} -
- ); + return null; }; return ( @@ -138,7 +131,7 @@ const Story: FunctionComponent = () => { {selectedStory?.slides.map( (currentSlide, index) => index === slideIndex && - (currentSlide.type === SlideType.Splashscreen ? ( + (currentSlide.splashImage ? ( { +export const useStoryGlobe = (globeItem: GlobeItem) => { const dispatch = useDispatch(); const defaultView = config.globe.view; // fly to position given in a slide, if none given set to default // set layer given by story slide useEffect(() => { - const [mainLayer, compareLayer] = slide.layer || []; + const [mainLayer, compareLayer] = globeItem.layer || []; const slideTime = mainLayer?.timestamp ? Number(new Date(mainLayer?.timestamp)) : 0; // eslint-disable-next-line no-warning-comments // FIXME: the stories are the last place where the old flyTo syntax is being used. const cameraView: CameraView = - slide.flyTo && flyToToCameraView(slide.flyTo); + globeItem.flyTo && flyToToCameraView(globeItem.flyTo); dispatch(setFlyToAction(cameraView || defaultView)); dispatch(setSelectedLayerIdsAction(mainLayer?.id || null, true)); dispatch(setSelectedLayerIdsAction(compareLayer?.id || null, false)); dispatch(setGlobeTimeAction(slideTime)); - }, [dispatch, defaultView, slide]); + }, [dispatch, defaultView, globeItem]); return; }; diff --git a/src/scripts/hooks/use-story-navigation.ts b/src/scripts/hooks/use-story-navigation.ts index ec2fb16ee..9b855bc18 100644 --- a/src/scripts/hooks/use-story-navigation.ts +++ b/src/scripts/hooks/use-story-navigation.ts @@ -2,17 +2,12 @@ import {useStoryParams} from './use-story-params'; import config from '../config/main'; import {StoryMode} from '../types/story-mode'; -import {SlideType} from '../types/slide-type'; +import {GalleryItemType} from '../types/gallery-item'; /* eslint-disable complexity */ export const useStoryNavigation = (videoDuration: number) => { - const { - mode, - storyIds, - storyIndex, - slideIndex, - selectedStory - } = useStoryParams(); + const {mode, storyIds, storyIndex, slideIndex, selectedStory} = + useStoryParams(); const numberOfSlides = selectedStory?.slides.length; let autoPlayLink = null; @@ -42,15 +37,16 @@ export const useStoryNavigation = (videoDuration: number) => { const currentSlide = selectedStory?.slides[slideIndex]; // go through all slides of one story - if (currentSlide?.images && currentSlide.type === SlideType.Image) { - delay = delay * currentSlide.images.length; - } - - if ( - (currentSlide?.videoId || currentSlide?.videoSrc) && - currentSlide.type === SlideType.Video - ) { - delay = delay + videoDuration; + if (currentSlide?.galleryItems.length) { + delay = currentSlide.galleryItems?.reduce((totalDelay, item) => { + if ( + item.type === GalleryItemType.Video && + (item?.videoId || item?.videoSrc) + ) { + return totalDelay + config.delay + videoDuration; + } + return totalDelay + config.delay; + }, 0); } if (slideIndex + 1 < numberOfSlides) { diff --git a/src/scripts/libs/convert-legacy-story.ts b/src/scripts/libs/convert-legacy-story.ts new file mode 100644 index 000000000..8b1efef6a --- /dev/null +++ b/src/scripts/libs/convert-legacy-story.ts @@ -0,0 +1,80 @@ +import { + EmbeddedItem, + GalleryItemType, + GlobeItem, + ImageItem, + VideoItem +} from '../types/gallery-item'; +import {LegacySlide, LegacyStory} from '../types/legacy-story'; +import {SlideType} from '../types/slide-type'; +import {Story} from '../types/story'; + +const getGalleryItems = (slide: LegacySlide) => { + if (slide.type === SlideType.Video) { + return [ + { + type: GalleryItemType.Video, + videoId: slide.videoId, + videoSrc: slide.videoSrc, + videoCaptions: slide.videoCaptions, + videoPoster: slide.videoPoster + } + ] as VideoItem[]; + } + + if (slide.type === SlideType.Image && slide.images) { + return slide.images.map((image, index) => ({ + type: GalleryItemType.Image, + imageCaption: slide.imageCaptions + ? slide.imageCaptions[index] + : // eslint-disable-next-line no-undefined + undefined, + image, + // eslint-disable-next-line no-undefined + imageFit: slide.imageFits ? slide.imageFits[index] : undefined + })) as ImageItem[]; + } + + if (slide.type === SlideType.Globe) { + return [ + { + type: GalleryItemType.Globe, + flyTo: slide.flyTo, + markers: slide.markers, + layer: slide.layer, + layerDescription: slide.layerDescription + } + ] as GlobeItem[]; + } + + if (slide.type === SlideType.Embedded) { + return [ + { + type: GalleryItemType.Embedded, + embeddedSrc: slide.embeddedSrc + } + ] as EmbeddedItem[]; + } + + return []; +}; + +/** + * Used to convert legacy story object coming from the API to the new internal story object + * + * @param story Legacy story object + * @returns Story object + */ +export const convertLegacyStory = (story: LegacyStory): Story => ({ + id: story.id, + slides: story.slides.map(slide => ({ + text: slide.text, + shortText: slide.shortText, + galleryItems: getGalleryItems(slide), + splashImage: + slide.images && slide.type === 'splashscreen' + ? slide.images[0] + : // eslint-disable-next-line no-undefined + undefined + })) +}); diff --git a/src/scripts/reducers/story/selected.ts b/src/scripts/reducers/story/selected.ts index d5c6fd128..aab0ea33b 100644 --- a/src/scripts/reducers/story/selected.ts +++ b/src/scripts/reducers/story/selected.ts @@ -3,6 +3,8 @@ import { FetchStoryActions } from '../../actions/fetch-story'; +import {convertLegacyStory} from '../../libs/convert-legacy-story'; + import {Story} from '../../types/story'; function selectedStoryReducer( @@ -11,7 +13,7 @@ function selectedStoryReducer( ): Story | null { switch (action.type) { case FETCH_STORY_SUCCESS: - return action.story; + return convertLegacyStory(action.story); default: return storyState; } diff --git a/src/scripts/types/gallery-item.ts b/src/scripts/types/gallery-item.ts new file mode 100644 index 000000000..98396231c --- /dev/null +++ b/src/scripts/types/gallery-item.ts @@ -0,0 +1,49 @@ +import {ImageFit} from './image-fit'; +import {Marker} from './marker-type'; +import {StoryLayer} from './story-layer'; + +export enum GalleryItemType { + Video = 'video', + Image = 'image', + Embedded = 'embedded', + Globe = 'globe' +} + +export interface ImageItem { + type: GalleryItemType.Image; + imageCaption?: string; + image: string; + imageFit?: ImageFit; +} + +export interface VideoItem { + type: GalleryItemType.Video; + videoId?: string; + videoSrc?: string[]; + videoCaptions?: string; + videoPoster?: string; +} + +export interface EmbeddedItem { + type: GalleryItemType.Embedded; + embeddedSrc?: string; +} + +export interface GlobeItem { + type: GalleryItemType.Globe; + flyTo: { + position: { + longitude: number; + latitude: number; + height: number; + }; + orientation: { + heading: number; + pitch: number; + roll: number; + }; + }; + markers: Marker[]; + layer?: StoryLayer[]; + layerDescription?: string; +} diff --git a/src/scripts/types/story.d.ts b/src/scripts/types/legacy-story.ts similarity index 85% rename from src/scripts/types/story.d.ts rename to src/scripts/types/legacy-story.ts index e80b2ce60..3c1066098 100644 --- a/src/scripts/types/story.d.ts +++ b/src/scripts/types/legacy-story.ts @@ -3,12 +3,7 @@ import {SlideType} from './slide-type'; import {Marker} from './marker-type'; import {ImageFit} from './image-fit'; -export interface Story { - id: string; - slides: Slide[]; -} - -export interface Slide { +export interface LegacySlide { type: SlideType; text: string; shortText?: string; @@ -19,6 +14,7 @@ export interface Slide { videoSrc?: string[]; videoCaptions?: string; videoPoster?: string; + embeddedSrc?: string; layer?: StoryLayer[]; layerDescription?: string; flyTo: { @@ -35,3 +31,8 @@ export interface Slide { }; markers: Marker[]; } + +export interface LegacyStory { + id: string; + slides: LegacySlide[]; +} diff --git a/src/scripts/types/slide-type.ts b/src/scripts/types/slide-type.ts index f0330a0ca..29d699eb3 100644 --- a/src/scripts/types/slide-type.ts +++ b/src/scripts/types/slide-type.ts @@ -2,5 +2,6 @@ export enum SlideType { Splashscreen = 'splashscreen', Video = 'video', Image = 'image', + Embedded = 'embedded', Globe = 'globe' } diff --git a/src/scripts/types/story.ts b/src/scripts/types/story.ts new file mode 100644 index 000000000..82e92ed52 --- /dev/null +++ b/src/scripts/types/story.ts @@ -0,0 +1,13 @@ +import {EmbeddedItem, GlobeItem, ImageItem, VideoItem} from './gallery-item'; + +export interface Slide { + text: string; + shortText?: string; + galleryItems: (ImageItem | VideoItem | EmbeddedItem | GlobeItem)[]; + splashImage?: string; +} + +export interface Story { + id: string; + slides: Slide[]; +} diff --git a/storage/stories/debug/debug-en.json b/storage/stories/debug/debug-en.json index 7ace46d0f..abaca137f 100644 --- a/storage/stories/debug/debug-en.json +++ b/storage/stories/debug/debug-en.json @@ -78,6 +78,12 @@ "text": "# YOUTUBE Stacking up the Data\n\nThe CCI Ozone team has worked on data from European and third party missions covering more than two decades of continuous ozone observations since 1995. Each space-borne sensor has its own radiometric characteristics, spatial resolution and coverage, making the harmonisation and merging of the data a complex task. The resulting integrated datasets have the advantage of providing better spatial coverage than those from individual sensors, and allow time series to exceed the life of a single instrument, giving the long-term trends so crucial for climate studies. They have enabled a better understanding of natural and anthropogenic factors affecting the distribution of atmospheric ozone and improved our understanding of ozone processes in climate models. \n\n![Ozone sensors](assets/ozone_large_09.png) \n_Satellites and sensors used by the CCI Ozone team. (update – extend time lines?)_\n\nJust as individuals can use daily UV and air quality warnings based on satellite data to protect their own health and that of their children, scientists are using the same observations from space to track the effect of ozone on the climate, so that political leaders have the information they need to make decisions and take action to protect us all. Emission controls will continue to reduce ozone destruction in the stratosphere and limit ozone creation in the troposphere, and provide successful examples of international cooperation to solve an environmental problem.", "shortText": "# Stacking up the Data\n\n(placeholder)", "videoId": "evPBTu_a27I" + }, + { + "type": "embedded", + "text": "## A Vital Element \r\n\r\nCarbon is the basic building block for life on Earth. It can form stable chemical bonds with many elements, allowing large and complex molecules to be built, including the organic compounds essential to life. Carbon’s bonds to other elements are stable, but not so strong that they prevent chemical reaction. \r\n\r\nAs it reacts with other elements, carbon cycles through the atmosphere, the oceans, plants and animals, soil and rocks. There are exchanges between these carbon reservoirs through a variety of processes. When carbon bonds are broken, energy is released, making some carbon compounds – hydrocarbons – convenient fuel sources. \r\n \r\n## Greenhouse Gases \r\n\r\nBut the same bonds that make carbon molecules so essential to life and modern living have a downside. They are also good at absorbing long-wavelength infrared radiation, allowing the molecules to vibrate and warm up, trapping heat in the atmosphere, contributing to the greenhouse effect. \r\n\r\nCarbon compounds such as carbon dioxide and methane are not the only greenhouse gases, nor the most powerful, but our increased burning of fossil fuels – coal, oil and natural gas – has caused an accumulation of carbon dioxide in the atmosphere, disrupting the carbon cycle, and warming the Earth’s climate.", + "shortText": "## A Vital Element \r\n\r\n- Carbon is the basic building block for life on Earth.\r\n- Forms stable bonds with many elements.\r\n- Allows large and complex molecules to be built, including organic compounds essential for life.\r\n- Bonds are stable, but not so strong that they prevent chemical reaction. \r\n- Reactions drive carbon through the atmosphere, the oceans, plants and animals, soil and rocks. \r\n- When carbon bonds are broken, energy is released.\r\n- So some carbon compounds – hydrocarbons – are convenient fuel sources. \r\n\r\n## Greenhouse Gases \r\n\r\n- Carbon bonds also good at absorbing infrared radiation, allowing molecules to vibrate and warm up.\r\n- Traps heat in the atmosphere, contributing to the greenhouse effect. \r\n\r\nIncreased burning of fossil fuels has caused an accumulation of CO2 in the atmosphere, disrupting the carbon cycle, and warming the climate.", + "embeddedSrc": "https://datawrapper.dwcdn.net/F0JkU/1/?transparent=true" } ] }