comments.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. <template lang="pug">
  2. div(v-intersect.once='onIntersect')
  3. v-textarea#discussion-new(
  4. outlined
  5. flat
  6. placeholder='Write a new comment...'
  7. auto-grow
  8. dense
  9. rows='3'
  10. hide-details
  11. v-model='newcomment'
  12. color='blue-grey darken-2'
  13. :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
  14. v-if='permissions.write'
  15. )
  16. v-row.mt-2(dense, v-if='!isAuthenticated && permissions.write')
  17. v-col(cols='12', lg='6')
  18. v-text-field(
  19. outlined
  20. color='blue-grey darken-2'
  21. :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
  22. placeholder='Your Name'
  23. hide-details
  24. dense
  25. autocomplete='name'
  26. v-model='guestName'
  27. )
  28. v-col(cols='12', lg='6')
  29. v-text-field(
  30. outlined
  31. color='blue-grey darken-2'
  32. :background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
  33. placeholder='Your Email Address'
  34. hide-details
  35. type='email'
  36. dense
  37. autocomplete='email'
  38. v-model='guestEmail'
  39. )
  40. .d-flex.align-center.pt-3(v-if='permissions.write')
  41. v-icon.mr-1(color='blue-grey') mdi-language-markdown-outline
  42. .caption.blue-grey--text Markdown Format
  43. v-spacer
  44. .caption.mr-3(v-if='isAuthenticated') Posting as #[strong {{userDisplayName}}]
  45. v-btn(
  46. dark
  47. color='blue-grey darken-2'
  48. @click='postComment'
  49. depressed
  50. )
  51. v-icon(left) mdi-comment
  52. span.text-none Post Comment
  53. v-divider.mt-3(v-if='permissions.write')
  54. .pa-5.d-flex.align-center.justify-center(v-if='isLoading && !hasLoadedOnce')
  55. v-progress-circular(
  56. indeterminate
  57. size='20'
  58. width='1'
  59. color='blue-grey'
  60. )
  61. .caption.blue-grey--text.pl-3: em Loading comments...
  62. v-timeline(
  63. dense
  64. v-else-if='comments && comments.length > 0'
  65. )
  66. v-timeline-item.comments-post(
  67. color='pink darken-4'
  68. large
  69. v-for='cm of comments'
  70. :key='`comment-` + cm.id'
  71. :id='`comment-post-id-` + cm.id'
  72. )
  73. template(v-slot:icon)
  74. v-avatar(color='blue-grey')
  75. //- v-img(src='http://i.pravatar.cc/64')
  76. span.white--text.title {{cm.initials}}
  77. v-card.elevation-1
  78. v-card-text
  79. .comments-post-actions(v-if='permissions.manage && !isBusy')
  80. v-icon.mr-3(small, @click='editComment(cm)') mdi-pencil
  81. v-icon(small, @click='deleteCommentConfirm(cm)') mdi-delete
  82. .comments-post-name.caption: strong {{cm.authorName}}
  83. .comments-post-date.overline.grey--text {{cm.createdAt | moment('from') }} #[em(v-if='cm.createdAt !== cm.updatedAt') - modified {{cm.updatedAt | moment('from') }}]
  84. .comments-post-content.mt-3(v-html='cm.render')
  85. .pt-5.text-center.body-2.blue-grey--text(v-else-if='permissions.write') Be the first to comment.
  86. .text-center.body-2.blue-grey--text(v-else) No comments yet.
  87. v-dialog(v-model='deleteCommentDialogShown', max-width='500')
  88. v-card
  89. .dialog-header.is-red Confirm Delete
  90. v-card-text.pt-5
  91. span Are you sure you want to permanently delete this comment?
  92. .caption: strong This action cannot be undone!
  93. v-card-chin
  94. v-spacer
  95. v-btn(text, @click='deleteCommentDialogShown = false') {{$t('common:actions.cancel')}}
  96. v-btn(color='red', dark, @click='deleteComment') {{$t('common:actions.delete')}}
  97. </template>
  98. <script>
  99. import gql from 'graphql-tag'
  100. import { get } from 'vuex-pathify'
  101. import validate from 'validate.js'
  102. import _ from 'lodash'
  103. export default {
  104. data () {
  105. return {
  106. newcomment: '',
  107. isLoading: true,
  108. hasLoadedOnce: false,
  109. comments: [],
  110. guestName: '',
  111. guestEmail: '',
  112. commentToDelete: {},
  113. deleteCommentDialogShown: false,
  114. isBusy: false,
  115. scrollOpts: {
  116. duration: 1500,
  117. offset: 0,
  118. easing: 'easeInOutCubic'
  119. }
  120. }
  121. },
  122. computed: {
  123. pageId: get('page/id'),
  124. permissions: get('page/commentsPermissions'),
  125. isAuthenticated: get('user/authenticated'),
  126. userDisplayName: get('user/name')
  127. },
  128. methods: {
  129. onIntersect (entries, observer, isIntersecting) {
  130. if (isIntersecting) {
  131. this.fetch(true)
  132. }
  133. },
  134. async fetch (silent = false) {
  135. this.isLoading = true
  136. try {
  137. const results = await this.$apollo.query({
  138. query: gql`
  139. query ($locale: String!, $path: String!) {
  140. comments {
  141. list(locale: $locale, path: $path) {
  142. id
  143. render
  144. authorName
  145. createdAt
  146. updatedAt
  147. }
  148. }
  149. }
  150. `,
  151. variables: {
  152. locale: this.$store.get('page/locale'),
  153. path: this.$store.get('page/path')
  154. },
  155. fetchPolicy: 'network-only'
  156. })
  157. this.comments = _.get(results, 'data.comments.list', []).map(c => {
  158. const nameParts = c.authorName.toUpperCase().split(' ')
  159. let initials = _.head(nameParts).charAt(0)
  160. if (nameParts.length > 1) {
  161. initials += _.last(nameParts).charAt(0)
  162. }
  163. c.initials = initials
  164. return c
  165. })
  166. } catch (err) {
  167. console.warn(err)
  168. if (!silent) {
  169. this.$store.commit('showNotification', {
  170. style: 'red',
  171. message: err.message,
  172. icon: 'alert'
  173. })
  174. }
  175. }
  176. this.isLoading = false
  177. this.hasLoadedOnce = true
  178. },
  179. /**
  180. * Post New Comment
  181. */
  182. async postComment () {
  183. let rules = {
  184. comment: {
  185. presence: {
  186. allowEmpty: false
  187. },
  188. length: {
  189. minimum: 2
  190. }
  191. }
  192. }
  193. if (!this.isAuthenticated && this.permissions.write) {
  194. rules.name = {
  195. presence: {
  196. allowEmpty: false
  197. },
  198. length: {
  199. minimum: 2,
  200. maximum: 255
  201. }
  202. }
  203. rules.email = {
  204. presence: {
  205. allowEmpty: false
  206. },
  207. email: true
  208. }
  209. }
  210. const validationResults = validate({
  211. comment: this.newcomment,
  212. name: this.guestName,
  213. email: this.guestEmail
  214. }, rules, { format: 'flat' })
  215. if (validationResults) {
  216. this.$store.commit('showNotification', {
  217. style: 'red',
  218. message: validationResults[0],
  219. icon: 'alert'
  220. })
  221. return
  222. }
  223. try {
  224. const resp = await this.$apollo.mutate({
  225. mutation: gql`
  226. mutation (
  227. $pageId: Int!
  228. $replyTo: Int
  229. $content: String!
  230. $guestName: String
  231. $guestEmail: String
  232. ) {
  233. comments {
  234. create (
  235. pageId: $pageId
  236. replyTo: $replyTo
  237. content: $content
  238. guestName: $guestName
  239. guestEmail: $guestEmail
  240. ) {
  241. responseResult {
  242. succeeded
  243. errorCode
  244. slug
  245. message
  246. }
  247. id
  248. }
  249. }
  250. }
  251. `,
  252. variables: {
  253. pageId: this.pageId,
  254. replyTo: 0,
  255. content: this.newcomment,
  256. guestName: this.guestName,
  257. guestEmail: this.guestEmail
  258. }
  259. })
  260. if (_.get(resp, 'data.comments.create.responseResult.succeeded', false)) {
  261. this.$store.commit('showNotification', {
  262. style: 'success',
  263. message: 'New comment posted successfully.',
  264. icon: 'check'
  265. })
  266. this.newcomment = ''
  267. await this.fetch()
  268. this.$nextTick(() => {
  269. this.$vuetify.goTo(`#comment-post-id-${_.get(resp, 'data.comments.create.id', 0)}`, this.scrollOpts)
  270. })
  271. } else {
  272. throw new Error(_.get(resp, 'data.comments.create.responseResult.message', 'An unexpected error occured.'))
  273. }
  274. } catch (err) {
  275. this.$store.commit('showNotification', {
  276. style: 'red',
  277. message: err.message,
  278. icon: 'alert'
  279. })
  280. }
  281. },
  282. async editComment (cm) {
  283. },
  284. deleteCommentConfirm (cm) {
  285. this.commentToDelete = cm
  286. this.deleteCommentDialogShown = true
  287. },
  288. /**
  289. * Delete Comment
  290. */
  291. async deleteComment () {
  292. this.$store.commit(`loadingStart`, 'comments-delete')
  293. this.isBusy = true
  294. this.deleteCommentDialogShown = false
  295. try {
  296. const resp = await this.$apollo.mutate({
  297. mutation: gql`
  298. mutation (
  299. $id: Int!
  300. ) {
  301. comments {
  302. delete (
  303. id: $id
  304. ) {
  305. responseResult {
  306. succeeded
  307. errorCode
  308. slug
  309. message
  310. }
  311. }
  312. }
  313. }
  314. `,
  315. variables: {
  316. id: this.commentToDelete.id
  317. }
  318. })
  319. if (_.get(resp, 'data.comments.delete.responseResult.succeeded', false)) {
  320. this.$store.commit('showNotification', {
  321. style: 'success',
  322. message: 'Comment was deleted successfully.',
  323. icon: 'check'
  324. })
  325. this.comments = _.reject(this.comments, ['id', this.commentToDelete.id])
  326. } else {
  327. throw new Error(_.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occured.'))
  328. }
  329. } catch (err) {
  330. this.$store.commit('showNotification', {
  331. style: 'red',
  332. message: err.message,
  333. icon: 'alert'
  334. })
  335. }
  336. this.isBusy = false
  337. this.$store.commit(`loadingStop`, 'comments-delete')
  338. }
  339. }
  340. }
  341. </script>
  342. <style lang="scss">
  343. .comments-post {
  344. position: relative;
  345. &:hover {
  346. .comments-post-actions {
  347. opacity: 1;
  348. }
  349. }
  350. &-actions {
  351. position: absolute;
  352. top: 16px;
  353. right: 16px;
  354. opacity: 0;
  355. transition: opacity .4s ease;
  356. }
  357. &-content {
  358. > p:first-child {
  359. padding-top: 0;
  360. }
  361. p {
  362. padding-top: 1rem;
  363. margin-bottom: 0;
  364. }
  365. img {
  366. max-width: 100%;
  367. border-radius: 5px;
  368. }
  369. code {
  370. background-color: rgba(mc('pink', '500'), .1);
  371. box-shadow: none;
  372. }
  373. pre > code {
  374. margin-top: 1rem;
  375. padding: 12px;
  376. background-color: #111;
  377. box-shadow: none;
  378. border-radius: 5px;
  379. width: 100%;
  380. color: #FFF;
  381. font-weight: 400;
  382. font-size: .85rem;
  383. font-family: Roboto Mono, monospace;
  384. }
  385. }
  386. }
  387. </style>