NEXT craftinamerica.org. Base setup for headless wordpress https://www.craftinamerica.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

list.vue 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <template lang="pug">
  2. .page--list.f-col.between
  3. article.f-grow
  4. header.center.t-up
  5. .title.f-row
  6. h3 {{ type }} list
  7. span(v-if="sortBy")
  8. h3 sorted {{ sortBy.replace('-', ' ') }}
  9. h3(v-if="!loaded") loading...
  10. .content(
  11. v-else-if="allPagesLoaded && type && allPages[type]"
  12. v-html="allPages[type].content"
  13. )
  14. ul.posts.f-col(v-if="posts && loaded" :class="{ 'is-grid': grid }")
  15. li(v-for="(post, i) in posts" :key="post.slug").post.shadow
  16. card(:content="post" :type="type" :wide="isWide")
  17. //- Important: Do NOT remove this! Required for intersection observer
  18. footer
  19. p(v-if="loadingFetched") loading more {{ type }}...
  20. p(v-if="showMeta") {{ `${type} count: ${Object.values(posts).length}` }}
  21. p(v-if="showMeta") {{ `show sidebar: ${sidebar}` }}
  22. sidebar(v-if="sidebar" :type="`${type}`" layout="list")
  23. </template>
  24. <script>
  25. import featuredImage from '@/components/featured-image'
  26. import card from '@/components/card'
  27. import sidebar from '@/components/sidebars/sidebar'
  28. import { postTypeGetters, scrollTop, heroUtils } from './mixin-post-types'
  29. import { postTypes, sortTypes, convertTitleCase, typeFromRoute, sortFromRoute } from '@/utils/helpers'
  30. const TIMEOUT = 1
  31. const INTERSECT_SELECTOR = '.page--list > article footer'
  32. export default {
  33. components: { sidebar, featuredImage, card },
  34. props: {
  35. sidebar: { type: Boolean },
  36. grid: { type: Boolean },
  37. sortBy: { type: String },
  38. },
  39. mixins: [postTypeGetters, scrollTop, heroUtils],
  40. data() {
  41. return {
  42. showMeta:false,
  43. page: 0,
  44. perPage: 21,
  45. keepFetching: true,
  46. loadingFetched: false,
  47. observer: null
  48. }
  49. },
  50. computed: {
  51. type() {
  52. // Checks for type and fixes Episodes route edge case
  53. return typeFromRoute(this.$route)
  54. },
  55. pType() {
  56. if(!typeFromRoute(this.$route)) return
  57. return this.sortBy ? `${convertTitleCase(typeFromRoute(this.$route).split('/')[0])}s` : `${convertTitleCase(this.type)}s`
  58. },
  59. loaded() {
  60. if (!this.pType) return
  61. return this[`all${this.pType}Loaded`]
  62. },
  63. posts() {
  64. if (!this.pType) return
  65. return this[`all${this.pType}`]
  66. },
  67. isWide() {
  68. const wide = ['exhibition', 'event']
  69. return wide.includes(this.type)
  70. }
  71. },
  72. methods: {
  73. clearAllPosts() {
  74. if(!this.type) return console.error(`type: ${this.type}...`)
  75. const uppercaseType = this.type.toUpperCase() + 'S'
  76. this.$store.commit(`CLEAR_${uppercaseType}`)
  77. this.$store.commit(`${uppercaseType}_LOADED`)
  78. },
  79. async loadMorePosts() {
  80. if(!this.type) return console.warn(`this.type: ${type} not found...`)
  81. if(!this.keepFetching) return console.warn('nothing left to fetch...')
  82. this.loadingFetched = true
  83. // console.warn(`loading page ${this.page + 1} of ${type} posts: ${this.page * this.perPage + 1} to ${(this.page + 1) * this.perPage}`)
  84. this.page++
  85. await this.getPosts()
  86. },
  87. _getSortBy() {
  88. return this.sortBy
  89. ? this.sortBy
  90. : sortFromRoute(this.$route)
  91. },
  92. _getDispatchParams(perPage) {
  93. return {
  94. sortType: this._getSortBy(),
  95. params: {
  96. limit: perPage ? perPage : this.perPage,
  97. page: this.page
  98. }
  99. }
  100. },
  101. async getPosts() {
  102. let dispatchAction = `getMore${this.pType}`
  103. let params = this._getDispatchParams()
  104. // For episodes, or material sort we grab EVERYTHING
  105. const getAllClause = this.type == 'episode' || this.type == 'artist' && this._getSortBy() == sortTypes.material
  106. if(getAllClause) {
  107. dispatchAction = `getAll${this.pType}`
  108. params = this._getDispatchParams(-1)
  109. // Clear sortType if this is an episodes list
  110. params.sortType = this.type == 'episode' ? null : params.sortType
  111. }
  112. let res = null
  113. // We always grab all pages on hero init so no need to do it here
  114. if(this.pType && this.keepFetching && this.type != 'page') {
  115. res = await this.$store.dispatch(dispatchAction, params)
  116. }
  117. // Stop trying to load more posts
  118. if(res && !res.length || getAllClause) {
  119. console.warn(`empty response for ${this.type}:`, res)
  120. this.keepFetching = false
  121. }
  122. this.loadingFetched = false
  123. },
  124. async getPageForType(type) {
  125. await this._getAll('page', this.$store)
  126. if(!this.allPages) return console.warn('no pages in state', this)
  127. const page = this.allPages.filter(page => page.slug == `${type}s`)[0]
  128. if(!page) return console.warn(`no page for ${type} found`)
  129. return page
  130. },
  131. // _setHeroInfo(post) {} from mixin
  132. // _clearHero(store) {} from mixin
  133. async checkAndSetHero(type) {
  134. this._clearHero(this.$store)
  135. const page = await this.getPageForType(type)
  136. // We always set a hero no matter what
  137. // Because the hero component will deal
  138. // with how to render based on hero.url
  139. this.$store.commit('SET_HERO', this._setHeroInfo(page))
  140. },
  141. setIntersectionLoader() {
  142. if(!this.type) return console.error('cannot setup intersection handler for undefined type')
  143. // KeepFetching is UNSET for certain post types and sort types in `loadMorePosts()`
  144. if(!this.keepFetching) return console.warn('cannot setup intersection handler keepFetching is set false')
  145. console.warn('Setting interesection loader...')
  146. // Always unset and reset the intersection loader
  147. this.unsetIntersectionLoader()
  148. const onIntersect = (entries, observer) => {
  149. entries.forEach(entry => {
  150. if (!entry.isIntersecting || this.loadingFetched) return
  151. setTimeout(() => this.loadMorePosts(), TIMEOUT)
  152. })
  153. }
  154. this.observer = new IntersectionObserver(onIntersect, { threshold: 0.80 })
  155. this.observer.observe(document.querySelector(INTERSECT_SELECTOR))
  156. },
  157. unsetIntersectionLoader() {
  158. const footerEl = document.querySelector(INTERSECT_SELECTOR)
  159. try {
  160. if(!footerEl) throw `cannot unset intersection handler missing el: ${footerEl}`
  161. if(!this.observer) throw `cannot unset intersection handler missing observer: ${this.observer}`
  162. this.observer.unobserve(footerEl)
  163. this.observer.disconnect()
  164. } catch (error) {
  165. console.error(error)
  166. }
  167. },
  168. clearAndInitPostList(caller) {
  169. console.log(`clearAndInitPostList from ${caller}...`)
  170. console.log('---')
  171. this.page = 0
  172. this.keepFetching = true
  173. this.checkAndSetHero(this.type)
  174. // Fires when loading from home
  175. // Clear any preloaded posts (from home, etc.)
  176. this.clearAllPosts()
  177. this.loadMorePosts()
  178. this.setIntersectionLoader()
  179. }
  180. },
  181. watch: {
  182. // This only fires navigating from
  183. // a list page, to another list page
  184. // and the post type has changed
  185. type(newType, oldType) {
  186. if(!postTypes.includes(newType)) return console.warn('type not found...')
  187. this.clearAndInitPostList('type watcher')
  188. },
  189. // Only fire if the sort type has changed
  190. // and the post type is the same
  191. // and both sorts are valid
  192. $route(to, from) {
  193. const validSorts = Object.values(sortTypes)
  194. if (
  195. typeFromRoute(to) == typeFromRoute(from) &&
  196. sortFromRoute(to) != sortFromRoute(from) &&
  197. // Post slug sometimes appears as a sort so we check it against known sorts
  198. validSorts.includes(sortFromRoute(to)) && validSorts.includes(sortFromRoute(from))
  199. ) {
  200. this.clearAndInitPostList('$route watcher')
  201. }
  202. }
  203. },
  204. mounted() {
  205. // This only fires navigating from a single page, to a list page
  206. this.clearAndInitPostList('mounted')
  207. },
  208. beforeDestroy() {
  209. this.unsetIntersectionLoader()
  210. }
  211. }
  212. </script>
  213. <style lang="postcss">
  214. // prettier-ignore
  215. @import '../sss/variables.sss'
  216. @import '../sss/theme.sss'
  217. .page--list article
  218. > header
  219. padding: 1em
  220. > h1
  221. margin: 0
  222. > .content
  223. padding: 0
  224. width: 100%
  225. > footer
  226. padding: $ms-0
  227. /* posts not grid list */
  228. .posts
  229. list-style: none
  230. grid-gap: $ms--2
  231. .post
  232. width: 100%
  233. /* posts in grid list */
  234. .posts.is-grid
  235. width: 100%
  236. display: grid
  237. grid-template-columns: repeat(1, 1fr)
  238. align-items: start
  239. /* This is important for how the grid lines up to the page */
  240. justify-content: right
  241. .post img
  242. width: 100%
  243. @media (min-width: $medium)
  244. .page--list
  245. &.f-col
  246. flex-direction: row
  247. > article
  248. margin: 0 $ms--2 0 0
  249. .posts.is-grid
  250. grid-template-columns: repeat(3, 1fr)
  251. </style>