News.vue 3.0 KB

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