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

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