News.vue 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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 { useEvents } from "@/composables/useEvents";
  8. import { useModels } from "@/composables/useModels";
  9. import { useWebsocketStore } from "@/stores/websocket";
  10. const MainHeader = defineAsyncComponent(
  11. () => import("@/components/MainHeader.vue")
  12. );
  13. const MainFooter = defineAsyncComponent(
  14. () => import("@/components/MainFooter.vue")
  15. );
  16. const { runJob } = useWebsocketStore();
  17. const { onReady } = useEvents();
  18. const { registerModel, registerModels, onCreated, onDeleted } = useModels();
  19. const news = ref<NewsModel[]>([]);
  20. const { sanitize } = DOMPurify;
  21. onMounted(async () => {
  22. marked.use({
  23. renderer: {
  24. table(header, body) {
  25. return `<table class="table">
  26. <thead>${header}</thead>
  27. <tbody>${body}</tbody>
  28. </table>`;
  29. }
  30. }
  31. });
  32. await onReady(async () => {
  33. news.value = await registerModels(
  34. await runJob("data.news.newest", {}),
  35. { news: "createdBy" }
  36. );
  37. });
  38. await onCreated("news", async ({ doc }) => {
  39. const newDoc = await registerModel(doc, { news: "createdBy" });
  40. news.value.unshift(newDoc);
  41. });
  42. await onDeleted("news", async ({ oldDoc }) => {
  43. const index = news.value.findIndex(doc => doc._id === oldDoc._id);
  44. if (index < 0) return;
  45. news.value.splice(index, 1);
  46. });
  47. });
  48. </script>
  49. <template>
  50. <div class="app">
  51. <page-metadata title="News" />
  52. <main-header />
  53. <div class="container">
  54. <div class="content-wrapper">
  55. <h1 class="has-text-centered page-title">News</h1>
  56. <div
  57. v-for="item in news"
  58. :key="item._id"
  59. class="section news-item"
  60. >
  61. <div v-html="sanitize(marked(item.markdown))"></div>
  62. <div class="info">
  63. <hr />
  64. By&nbsp;
  65. <router-link
  66. :to="{ path: `/u/${item.createdBy.username}` }"
  67. :title="item.createdBy._id"
  68. >
  69. {{ item.createdBy.name }} </router-link
  70. >&nbsp;
  71. <span :title="new Date(item.createdAt).toString()">
  72. {{
  73. formatDistance(
  74. new Date(item.createdAt),
  75. new Date(),
  76. {
  77. addSuffix: true
  78. }
  79. )
  80. }}
  81. </span>
  82. </div>
  83. </div>
  84. <h3 v-if="news.length === 0" class="has-text-centered">
  85. No news items were found.
  86. </h3>
  87. </div>
  88. </div>
  89. <main-footer />
  90. </div>
  91. </template>
  92. <style lang="less" scoped>
  93. .night-mode {
  94. p {
  95. color: var(--light-grey-2);
  96. }
  97. }
  98. .container {
  99. width: calc(100% - 32px);
  100. }
  101. .section {
  102. border: 1px solid var(--light-grey-3);
  103. max-width: 100%;
  104. margin-top: 50px;
  105. &:last-of-type {
  106. margin-bottom: 50px;
  107. }
  108. }
  109. </style>