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.

single.vue 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. <template lang="pug">
  2. .page--single.f-col.between
  3. gallery(v-if="activeGalleryIndex >= 0" :activeImageIndex="activeImageIndex" :images="imagesInGallery" @close="closeGallery")
  4. article.w-max.f-grow.shadow(v-if="!singlePost || loading")
  5. header
  6. p loading...
  7. article(v-else).w-max.f-grow.shadow
  8. header
  9. //- breadcrumb links at top of page, needs link routing
  10. breadcrumb(:type="type" :post="singlePost")
  11. h1.t-b {{ singlePost.title }}
  12. //- p(v-if="singlePost.categories") categories: {{ singlePost.categories }}
  13. //- p(v-if="singlePost.type") type: {{ singlePost.type }}
  14. //- p(v-if="singlePost.subtypes") subtypes: {{ singlePost.subtypes }}
  15. .date-info.t-cntr(v-if="['exhibition', 'event'].includes(type)")
  16. //- for events display: date, time-time
  17. h4(v-if="singlePost.start, singlePost.end && type == 'event'") {{ dateFrom(singlePost.start, type == 'event') }} - {{ dateFrom(singlePost.end, type == 'event').split(',')[1] }}
  18. //- else for single, exhibition: date-date
  19. h4(v-else-if="singlePost.start, singlePost.end") {{ dateFrom(singlePost.start, type == 'event') }} - {{ dateFrom(singlePost.end, type == 'event') }}
  20. //- WP main content
  21. section.content(v-html="singlePost.content")
  22. //- related artists section for episodes
  23. section(v-if="type === 'episode' && post").related-artists
  24. h2.t-up featured in this episode
  25. ul
  26. li.f-row.between(v-for="artist in p2pPostsByType['artist']")
  27. card(:content="artist" type="artist" :wide="true" :hide-type="true")
  28. credits(v-if="type === 'episode' && singlePost" :post="singlePost")
  29. //- end of article icon
  30. footer.f-col
  31. img(src="../star.svg")
  32. sidebar(:type="`${type}`" layout="single" :related="p2pPostsByType")
  33. </template>
  34. <script>
  35. import card from '@/components/card.vue'
  36. import sidebar from '@/components/sidebars/sidebar'
  37. import gallery from '@/components/gallery/'
  38. import credits from '@/components/credits'
  39. import breadcrumb from '@/components/breadcrumb'
  40. import { postTypeGetters, scrollTop, heroUtils } from './mixin-post-types'
  41. import { postTypes, convertTitleCase, formatDate } from '@/utils/helpers'
  42. const TIMEOUT = 1
  43. export default {
  44. components: { sidebar, gallery, credits, card, breadcrumb },
  45. mixins: [postTypeGetters, scrollTop, heroUtils],
  46. data() {
  47. return {
  48. // Gallery control
  49. activeGalleryIndex: -1,
  50. activeImageID: -1,
  51. loading: true
  52. }
  53. },
  54. computed: {
  55. type() {
  56. return postTypes.includes(this.$route.params.type) ? this.$route.params.type : 'post'
  57. },
  58. slug() {
  59. return this.$route.params.slug
  60. },
  61. /**
  62. * We get the actual post data using the slug
  63. * Careful with name collisions with vuex helpers
  64. */
  65. singlePost() {
  66. const postType = this.type == 'blog' ? 'post': this.type
  67. if (!this[postType]) return
  68. // State not a getter!
  69. const singleOfTypeFromState =
  70. this[postType][`single${convertTitleCase(postType)}`]
  71. if (!singleOfTypeFromState) return
  72. return singleOfTypeFromState
  73. },
  74. idsForGallery() {
  75. if (!this.singlePost || this.activeGalleryIndex < 0) return []
  76. return this.singlePost.galleries[this.activeGalleryIndex].ids
  77. },
  78. /**
  79. * We need a convenient way to get all the images
  80. * broken down by gallery. We use the active gallery
  81. * image IDs to create a map. We match the ID to the
  82. * image size and url information returned by singlePost.attached
  83. */
  84. imagesInGallery() {
  85. if (!this.activeGalleryIndex < 0) return {}
  86. return this.idsForGallery.reduce((imageMap, id) => {
  87. imageMap[id] = this.singlePost.attached[parseInt(id)]
  88. return imageMap
  89. }, {})
  90. },
  91. activeImageIndex() {
  92. return Object.keys(this.imagesInGallery).indexOf(
  93. this.activeImageID.toString(),
  94. )
  95. },
  96. p2pPostsByType() {
  97. return this.singlePost && this.singlePost.relatedto
  98. ? Object.values(this.singlePost.relatedto).reduce(
  99. (byType, relatedPost) => {
  100. if (!byType[relatedPost.type])
  101. byType[relatedPost.type] = []
  102. byType[relatedPost.type].push(relatedPost)
  103. return byType
  104. },
  105. {},
  106. )
  107. : {}
  108. },
  109. },
  110. methods: {
  111. /**
  112. * We set the active gallery to the index.
  113. * Everything kicks off when activeGallery
  114. * is set. We also need to set the activeImageID
  115. * to the image clicked
  116. * @param {string} imageInfo
  117. */
  118. openGallery(imageInfo) {
  119. const byIndex = this.singlePost.galleries.reduce(
  120. (byIndex, gallery, index) => {
  121. byIndex[index] = gallery.ids
  122. return byIndex
  123. },
  124. {},
  125. )
  126. let matchingIndex = 0
  127. Object.keys(byIndex).forEach(galleryIndex => {
  128. if (
  129. byIndex[galleryIndex].includes(
  130. parseInt(imageInfo.dataset.id)
  131. )
  132. )
  133. matchingIndex = galleryIndex
  134. })
  135. this.activeGalleryIndex = matchingIndex
  136. this.activeImageID = imageInfo.dataset.id
  137. ? parseInt(imageInfo.dataset.id)
  138. : parseInt(imageInfo.className.split('-').pop())
  139. },
  140. closeGallery() {
  141. this.activeGalleryIndex = this.activeImageID = -1
  142. },
  143. // _setHeroInfo(post) {} from mixin
  144. // _clearHero(store) {} from mixin
  145. /**
  146. * Everytime the post object changes
  147. * we use this to set a new HERO
  148. * in vuex
  149. * @param {object} post
  150. */
  151. checkAndSetHero(post) {
  152. this._clearHero(this.$store)
  153. if (!post) throw `No post found. Cannot set hero.`
  154. this.$store.commit('SET_HERO', this._setHeroInfo(post))
  155. },
  156. /**
  157. * Date Object from unix strings from db
  158. */
  159. dateFrom: (unix, includeTime) => formatDate(unix, includeTime),
  160. async loadPostData() {
  161. this.loading = true
  162. /**
  163. * Conditionally load based on post type
  164. * which is derived from the route
  165. */
  166. // modules are NOT plural because module key
  167. const postType = this.type == 'blog' ? 'post': this.type
  168. if (!this.$store.state[postType]) return
  169. const allPostsOfTypeInStore = this.$store.state[postType].all
  170. /**
  171. * Load posts if they're not already in state
  172. */
  173. // Find the single post from api if it's not already in state
  174. // Then add it to our list
  175. let singlePostData = allPostsOfTypeInStore.filter(
  176. post => post.slug == this.slug,
  177. )[0]
  178. // Look if it exists before you try and load everything!
  179. if (!singlePostData) {
  180. console.warn('Could not find single post in store; Fetching everything...')
  181. const res = await this.$store.dispatch(
  182. `getAll${convertTitleCase(postType)}s`,
  183. { sortType: null, params: null }
  184. )
  185. singlePostData = res.filter(
  186. post => post.slug == this.slug,
  187. )[0]
  188. }
  189. /**
  190. * At the point we MUST have singlePostData
  191. */
  192. try {
  193. this.checkAndSetHero(singlePostData)
  194. await this.$store.dispatch(
  195. `getSingle${convertTitleCase(postType)}`,
  196. singlePostData.id,
  197. )
  198. } catch (err) {
  199. console.error(err)
  200. }
  201. this.loading = false
  202. },
  203. },
  204. watch: {
  205. slug(newSlug, oldSlug) {
  206. // ONLY load post data when navigating TO a single page
  207. // OR when navigating TO a single page from a single page
  208. if(newSlug && !oldSlug || newSlug && oldSlug) {
  209. this._clearHero(this.$store)
  210. this.loadPostData()
  211. }
  212. },
  213. },
  214. created() {
  215. this.loadPostData()
  216. },
  217. }
  218. </script>
  219. <style lang="postcss">
  220. // prettier-ignore
  221. @import '../sss/variables.sss'
  222. @import '../sss/theme.sss'
  223. .page--single
  224. article
  225. background-color: white
  226. padding: $ms-0
  227. h1
  228. /* color: $cia_black */
  229. margin: 0 0 $ms--3 0
  230. > ul
  231. /* grid-gap: $ms-0 */
  232. list-style: none
  233. /* change to a 1/3 width of the article*/
  234. img.feature
  235. width: 20em
  236. /* wp-block-embed youtube link */
  237. .wp-block-embed, .is-type-video
  238. position: relative
  239. width: 100%
  240. padding-bottom: 56.25%
  241. margin-bottom: 7em
  242. &__wrapper
  243. display: contents
  244. /* TBD if kept- edit ot test */
  245. figcaption
  246. position: absolute
  247. top: 100%
  248. .wp-block-gallery
  249. margin: 0 0 0.5em 0
  250. grid-gap: $ms--5
  251. > .wp-block-image
  252. figcaption
  253. position: inherit
  254. background: none
  255. color: $cia_black
  256. /* iframe container 16:9 */
  257. [class^="iframe-container"]
  258. position: relative
  259. padding-bottom: 56.25%
  260. /* width: 100% */
  261. /* iframe container portrait */
  262. .iframe-container-v
  263. height: 100%
  264. padding-bottom: 125%
  265. iframe
  266. position: absolute
  267. top: 0
  268. left: 0
  269. width: 100%
  270. height: 100%
  271. /* separator styles */
  272. * hr
  273. margin: $ms-2 auto
  274. &.is-style
  275. &-default
  276. width: 15vw
  277. &-wide
  278. width: 50vw
  279. &-dots::before
  280. outline-style: none
  281. font-weight: bolder
  282. letter-spacing: $ms-8
  283. padding-left: $ms-8
  284. /* margin to indent marker */
  285. li
  286. margin: 0 0 $ms--2 $ms-4
  287. .related-artists
  288. ul li
  289. margin: 0 0 $ms-0 0
  290. .card
  291. padding: 0 0 $ms--2
  292. min-height: auto
  293. breadcrumb
  294. h5
  295. color: $cia_red
  296. //- end of article icon
  297. footer
  298. padding: $ms-6 0
  299. img
  300. height: $ms-3
  301. width: $ms-3
  302. @media (min-width: $medium)
  303. .page--single
  304. > article
  305. margin: 0 0.65em 0 0
  306. &.f-col
  307. flex-direction: row
  308. .wp-block-embed, .is-type-video
  309. margin-bottom: $ms-9
  310. </style>