Source

components/infinite-story-base.js

  1. import React from "react";
  2. import get from "lodash/get";
  3. import { InfiniteScroll } from "./infinite-scroll.js";
  4. import { removeDuplicateStories } from '../utils';
  5. /**
  6. * This component can be used to implement InfiniteScroll on the story page. You will need to specify the function which renders the story (which will recieve props.index and props.story), and functions for triggering analytics.
  7. *
  8. * Example
  9. * ```javascript
  10. * import React from 'react';
  11. *
  12. * import { BlankStory } from './story-templates';
  13. * import { InfiniteStoryBase } from '@quintype/components';
  14. *
  15. * function StoryPageBase({index, story, otherProp}) {
  16. * // Can switch to a different template based story-template, or only show a spoiler if index > 0
  17. * return <BlankStory story={story} />
  18. * }
  19. *
  20. * const FIELDS = "id,headline,slug,url,hero-image-s3-key,hero-image-metadata,first-published-at,last-published-at,alternative,published-at,author-name,author-id,sections,story-template,tags,cards";
  21. * function storyPageLoadItems(pageNumber) {
  22. * return global.superagent
  23. * .get("/api/v1/stories", {fields: FIELDS, limit:5, offset:5*pageNumber})
  24. * .then(response => response.body.stories.map(story => ({story: story, otherProp: "value"})));
  25. * }
  26. *
  27. * export function StoryPage(props) {
  28. * return <InfiniteStoryBase {...props}
  29. * render={StoryPageBase}
  30. * loadItems={storyPageLoadItems}
  31. * onItemFocus={(item) => console.log(`Story In View: ${item.story.headline}`)}
  32. * onInitialItemFocus={(item) => console.log(`Do Analytics ${item.story.headline}`)} />
  33. * }
  34. *
  35. * ```
  36. *
  37. * #### Not changing the URL on every page
  38. * When the next story is focussed, the url and title of the page will be set to the next story. If this is not required, it can be disabled by setting the prop doNotChangeUrl={true}.
  39. * This is typically used when showing the original story, followed by previews of subsequent stories.
  40. *
  41. * Example:
  42. * ```javascript
  43. * <InfiniteStoryBase {...props}
  44. * render={StoryPageBase}
  45. * loadItems={storyPageLoadItems}
  46. * onItemFocus={(item) => console.log(`Story In View: ${item.story.headline}`)}
  47. * doNotChangeUrl={true} />
  48. * ```
  49. *
  50. * #### Configuring the the url to change
  51. * When a story is focussed, the url is changed to the original slug of the story by default. To configure this, pass a prop called changeUrlTo as a function which returns the desired url.
  52. * This is typically used when you want to change the url but not to the original slug.
  53. *
  54. * Example:
  55. * ```javascript
  56. * <InfiniteStoryBase {...props}
  57. * render={StoryPageBase}
  58. * loadItems={storyPageLoadItems}
  59. * onItemFocus={(item) => console.log(`Story In View: ${item.story.headline}`)}
  60. changeUrlTo={(item) => item.currentPath || props.currentPath}
  61. * doNotChangeUrl={true} />
  62. * ```
  63. *
  64. * @component
  65. * @category Story Page
  66. */
  67. export class InfiniteStoryBase extends React.Component {
  68. constructor(props) {
  69. super(props);
  70. this.state = {
  71. moreItems: [],
  72. loading: false,
  73. pageNumber: 0,
  74. seenStoryIds: [props.data.story.id]
  75. }
  76. }
  77. allItems() {
  78. return [this.props.data].concat(this.state.moreItems);
  79. }
  80. onFocus(index) {
  81. const item = this.allItems()[index];
  82. if(!this.props.doNotChangeUrl) {
  83. const storyPath = item.story.url ? new URL(item.story.url).pathname : "/" + item.story.slug;
  84. const metaTitle = get(item, ['story', 'seo', 'meta-title'], item.story.headline);
  85. const title = get(item, ["customSeo","title"], metaTitle);
  86. const path = this.props.changeUrlTo ? this.props.changeUrlTo(item) : storyPath;
  87. global.app.maybeSetUrl(path, title);
  88. }
  89. this.props.onItemFocus && this.props.onItemFocus(item, index);
  90. if(!this.state.seenStoryIds.includes(item.story.id)) {
  91. this.setState({seenStoryIds: this.state.seenStoryIds.concat([item.story.id])}, () => {
  92. this.props.onInitialItemFocus && this.props.onInitialItemFocus(item, index);
  93. })
  94. }
  95. }
  96. loadMore() {
  97. if(this.state.loading)
  98. return;
  99. const pageNumber = this.state.pageNumber;
  100. const story = get(this.props.data, ['story'], {});
  101. this.setState({loading: true, pageNumber: pageNumber + 1}, () => {
  102. this.props.loadItems(pageNumber, story, this.props.config).then((items) => {
  103. this.setState({
  104. loading: false,
  105. moreItems: this.state.moreItems.concat(removeDuplicateStories(this.allItems(), items, item => item.story.id))
  106. })
  107. })
  108. })
  109. }
  110. render() {
  111. return <InfiniteScroll render={this.props.render}
  112. items={this.allItems()}
  113. loadNext={() => this.loadMore()}
  114. loadMargin={this.props.loadMargin}
  115. focusCallbackAt={this.props.focusCallbackAt || 20}
  116. onFocus={(index) => this.onFocus(index)}
  117. neverHideItem={this.props.neverHideItem}/>
  118. }
  119. }