NEXT craftinamerica.org. Base setup for headless wordpress https://www.craftinamerica.org
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

single.vue 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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, type == 'event') }}
  17. p end: {{ dateFrom(singlePost.end, type == 'event') }}
  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' && singlePost" :post="singlePost")
  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, scrollTop, heroUtils } from './mixin-post-types'
  40. import { sortTypes, 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, heroUtils],
  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. // _setHeroInfo(post) {} from mixin
  145. // _clearHero(store) {} from mixin
  146. /**
  147. * Everytime the posts object changes
  148. * we use this to set a new HERO
  149. * in vuex
  150. * @param {object} posts
  151. */
  152. checkAndSetHero(post) {
  153. this._clearHero(this.$store)
  154. if (!post) return console.warn(`no post found`)
  155. this.$store.commit('SET_HERO', this._setHeroInfo(post))
  156. },
  157. /**
  158. * Date Object from unix strings from db
  159. */
  160. dateFrom(unix, includeTime) {
  161. const d = new Date(parseInt(unix) * 1000)
  162. return includeTime ? d.toLocaleString('en-US', { timeZone: 'UTC' }) : d.toLocaleDateString('en-US')
  163. },
  164. async loadPostData() {
  165. /**
  166. * Conditionally load based on post type
  167. * which is derived from the route
  168. */
  169. // modules are NOT plural because module key
  170. if (!this.$store.state[this.type]) return
  171. let allPostsOfType = this.$store.state[this.type].all
  172. /**
  173. * Load posts if they're not already in state
  174. */
  175. // Find the single post from api if it's not already in state
  176. // Then add it to our list
  177. let singlePostData = allPostsOfType.filter(
  178. post => post.slug == this.$route.params.slug,
  179. )[0]
  180. // Look if it exists before you try and load everything!
  181. if(!singlePostData) {
  182. const res = await this.$store.dispatch(`getAll${convertTitleCase(this.type)}s`, { sortType: null, params: null})
  183. allPostsOfType = res
  184. singlePostData = allPostsOfType.filter(
  185. post => post.slug == this.$route.params.slug,
  186. )[0]
  187. }
  188. if (!singlePostData) return console.error('could not get single post data...')
  189. // NOT plural
  190. this.checkAndSetHero(singlePostData)
  191. await this.$store.dispatch(
  192. `getSingle${convertTitleCase(this.type)}`,
  193. singlePostData.id,
  194. )
  195. this.loading = false
  196. },
  197. },
  198. watch: {
  199. $route(to, from) {
  200. // Only load post data when
  201. // navigating TO a single page
  202. const path = to.fullPath.split('/').filter(p => p)
  203. const hasSort = path.filter(fragment => Object.values(sortTypes).includes(fragment))
  204. console.log('hasSort :', hasSort)
  205. console.log('path :', path)
  206. if (path.length < 1 && hasSort.length < 1) {
  207. this._clearHero(this.$store)
  208. this.loadPostData()
  209. }
  210. },
  211. },
  212. created() {
  213. this.loadPostData()
  214. },
  215. }
  216. </script>
  217. <style lang="postcss">
  218. // prettier-ignore
  219. @import '../sss/variables.sss'
  220. @import '../sss/theme.sss'
  221. .page--single
  222. article
  223. background-color: white
  224. padding: $ms-0
  225. h1
  226. color: $cia_black
  227. /* font-weight: 800 */
  228. /* padding: $ms--3 0 */
  229. > ul
  230. /* grid-gap: $ms-0 */
  231. list-style: none
  232. /* change to a 1/3 width of the article*/
  233. img.feature
  234. width: 20em
  235. li
  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. &__wrapper
  242. display: contents
  243. /* TBD if kept */
  244. figcaption
  245. position: absolute
  246. top: 39%
  247. margin: 0 3em 0
  248. color: aqua
  249. backdrop-filter: brightness(0.5)
  250. /* iframe container 16:9 */
  251. .iframe-container
  252. position: relative
  253. width: 100%
  254. padding-bottom: 56.25%
  255. /* iframe container portrait */
  256. .iframe-container-v
  257. position: relative
  258. width: 100%
  259. height: 100%
  260. padding-bottom: 125%
  261. iframe
  262. position: absolute
  263. top: 0px
  264. left: 0px
  265. width: 100%
  266. height: 100%
  267. /* separator styles */
  268. * hr
  269. margin: $ms-2 auto
  270. &.is-style
  271. &-default
  272. height: 1px
  273. width: 15vw
  274. &-wide
  275. height: 3px
  276. width: 50vw
  277. &-dots::before
  278. outline-style: none
  279. font-weight: bolder
  280. letter-spacing: $ms-8
  281. padding-left: $ms-8
  282. /* margin to indent marker */
  283. li
  284. margin: 0 0 $ms--2 $ms-4
  285. breadcrumb
  286. h5
  287. /* color: yellow */
  288. color: $cia_red
  289. /* font-weight: 400 */
  290. /* padding: $ms--6 0 */
  291. //- end of article icon
  292. footer
  293. padding: $ms-6 0
  294. img
  295. height: $ms-3
  296. width: $ms-3
  297. @media (min-width: $medium)
  298. .page--single.f-col
  299. flex-direction: row
  300. </style>