News.vue 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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. });
  46. socket.on("event:news.created", (res: NewsCreatedResponse) =>
  47. news.value.unshift(res.data.news)
  48. );
  49. socket.on("event:news.updated", (res: NewsUpdatedResponse) => {
  50. if (res.data.news.status === "draft") {
  51. news.value = news.value.filter(
  52. item => item._id !== res.data.news._id
  53. );
  54. return;
  55. }
  56. for (let n = 0; n < news.value.length; n += 1) {
  57. if (news.value[n]._id === res.data.news._id)
  58. news.value[n] = {
  59. ...news.value[n],
  60. ...res.data.news
  61. };
  62. }
  63. });
  64. socket.on("event:news.deleted", (res: NewsRemovedResponse) => {
  65. news.value = news.value.filter(item => item._id !== res.data.newsId);
  66. });
  67. });
  68. </script>
  69. <template>
  70. <div class="app">
  71. <page-metadata title="News" />
  72. <main-header />
  73. <div class="container">
  74. <div class="content-wrapper">
  75. <h1 class="has-text-centered page-title">News</h1>
  76. <div
  77. v-for="item in news"
  78. :key="item._id"
  79. class="section news-item"
  80. >
  81. <div v-html="sanitize(marked(item.markdown))"></div>
  82. <div class="info">
  83. <hr />
  84. By
  85. <user-link
  86. :user-id="item.createdBy"
  87. :alt="item.createdBy"
  88. />&nbsp;<span
  89. :title="new Date(item.createdAt).toString()"
  90. >
  91. {{
  92. formatDistance(item.createdAt, new Date(), {
  93. addSuffix: true
  94. })
  95. }}
  96. </span>
  97. </div>
  98. </div>
  99. <h3 v-if="news.length === 0" class="has-text-centered">
  100. No news items were found.
  101. </h3>
  102. </div>
  103. </div>
  104. <main-footer />
  105. </div>
  106. </template>
  107. <style lang="less" scoped>
  108. .night-mode {
  109. p {
  110. color: var(--light-grey-2);
  111. }
  112. }
  113. .container {
  114. width: calc(100% - 32px);
  115. }
  116. .section {
  117. border: 1px solid var(--light-grey-3);
  118. max-width: 100%;
  119. margin-top: 50px;
  120. &:last-of-type {
  121. margin-bottom: 50px;
  122. }
  123. }
  124. </style>