News.vue 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. <template>
  2. <div class="app">
  3. <metadata title="News" />
  4. <main-header />
  5. <div class="container">
  6. <div class="content-wrapper">
  7. <h1 class="has-text-centered page-title">News</h1>
  8. <div
  9. v-for="item in news"
  10. :key="item._id"
  11. class="section news-item"
  12. >
  13. <div v-html="sanitize(marked(item.markdown))"></div>
  14. <div class="info">
  15. <hr />
  16. By
  17. <user-id-to-username
  18. :user-id="item.createdBy"
  19. :alt="item.createdBy"
  20. :link="true"
  21. />
  22. <span :title="new Date(item.createdAt)">
  23. {{
  24. formatDistance(item.createdAt, new Date(), {
  25. addSuffix: true
  26. })
  27. }}
  28. </span>
  29. </div>
  30. </div>
  31. <h3 v-if="news.length === 0" class="has-text-centered">
  32. No news items were found.
  33. </h3>
  34. </div>
  35. </div>
  36. <main-footer />
  37. </div>
  38. </template>
  39. <script>
  40. import { formatDistance } from "date-fns";
  41. import { mapGetters } from "vuex";
  42. import marked from "marked";
  43. import { sanitize } from "dompurify";
  44. import ws from "@/ws";
  45. import MainHeader from "@/components/layout/MainHeader.vue";
  46. import MainFooter from "@/components/layout/MainFooter.vue";
  47. import UserIdToUsername from "@/components/UserIdToUsername.vue";
  48. export default {
  49. components: { MainHeader, MainFooter, UserIdToUsername },
  50. data() {
  51. return {
  52. news: []
  53. };
  54. },
  55. computed: mapGetters({
  56. socket: "websockets/getSocket"
  57. }),
  58. mounted() {
  59. marked.use({
  60. renderer: {
  61. table(header, body) {
  62. return `<table class="table is-striped">
  63. <thead>${header}</thead>
  64. <tbody>${body}</tbody>
  65. </table>`;
  66. }
  67. }
  68. });
  69. this.socket.dispatch("news.index", res => {
  70. if (res.status === "success") this.news = res.data.news;
  71. });
  72. this.socket.on("event:news.created", res =>
  73. this.news.unshift(res.data.news)
  74. );
  75. this.socket.on("event:news.updated", res => {
  76. if (res.data.news.status === "draft") {
  77. this.news = this.news.filter(
  78. item => item._id !== res.data.news._id
  79. );
  80. return;
  81. }
  82. for (let n = 0; n < this.news.length; n += 1) {
  83. if (this.news[n]._id === res.data.news._id)
  84. this.$set(this.news, n, {
  85. ...this.news[n],
  86. ...res.data.news
  87. });
  88. }
  89. });
  90. this.socket.on("event:news.deleted", res => {
  91. this.news = this.news.filter(item => item._id !== res.data.newsId);
  92. });
  93. if (this.socket.readyState === 1) this.init();
  94. ws.onConnect(() => this.init());
  95. },
  96. methods: {
  97. marked,
  98. sanitize,
  99. formatDistance,
  100. init() {
  101. this.socket.dispatch("apis.joinRoom", "news");
  102. }
  103. }
  104. };
  105. </script>
  106. <style lang="scss" scoped>
  107. .night-mode {
  108. p {
  109. color: var(--light-grey-2);
  110. }
  111. }
  112. .section {
  113. border: 1px solid var(--light-grey-3);
  114. width: 1000px;
  115. max-width: 100%;
  116. margin-top: 50px;
  117. &:last-of-type {
  118. margin-bottom: 50px;
  119. }
  120. }
  121. </style>