Report.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from "vue";
  3. import Toast from "toasters";
  4. import { storeToRefs } from "pinia";
  5. import { useWebsocketsStore } from "@/stores/websockets";
  6. import { useModalsStore } from "@/stores/modals";
  7. import { useReportStore } from "@/stores/report";
  8. const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
  9. const SongItem = defineAsyncComponent(
  10. () => import("@/components/SongItem.vue")
  11. );
  12. const ReportInfoItem = defineAsyncComponent(
  13. () => import("@/components/ReportInfoItem.vue")
  14. );
  15. const props = defineProps({
  16. modalUuid: { type: String, default: "" }
  17. });
  18. const { socket } = useWebsocketsStore();
  19. const reportStore = useReportStore(props);
  20. const { song } = storeToRefs(reportStore);
  21. const { openModal, closeCurrentModal } = useModalsStore();
  22. const existingReports = ref([]);
  23. const customIssues = ref([]);
  24. const predefinedCategories = ref([
  25. {
  26. category: "video",
  27. issues: [
  28. {
  29. enabled: false,
  30. title: "Doesn't exist",
  31. description: "",
  32. showDescription: false
  33. },
  34. {
  35. enabled: false,
  36. title: "It's private",
  37. description: "",
  38. showDescription: false
  39. },
  40. {
  41. enabled: false,
  42. title: "It's not available in my country",
  43. description: "",
  44. showDescription: false
  45. },
  46. {
  47. enabled: false,
  48. title: "Unofficial",
  49. description: "",
  50. showDescription: false
  51. }
  52. ]
  53. },
  54. {
  55. category: "title",
  56. issues: [
  57. {
  58. enabled: false,
  59. title: "Incorrect",
  60. description: "",
  61. showDescription: false
  62. },
  63. {
  64. enabled: false,
  65. title: "Inappropriate",
  66. description: "",
  67. showDescription: false
  68. }
  69. ]
  70. },
  71. {
  72. category: "duration",
  73. issues: [
  74. {
  75. enabled: false,
  76. title: "Skips too soon",
  77. description: "",
  78. showDescription: false
  79. },
  80. {
  81. enabled: false,
  82. title: "Skips too late",
  83. description: "",
  84. showDescription: false
  85. },
  86. {
  87. enabled: false,
  88. title: "Starts too soon",
  89. description: "",
  90. showDescription: false
  91. },
  92. {
  93. enabled: false,
  94. title: "Starts too late",
  95. description: "",
  96. showDescription: false
  97. }
  98. ]
  99. },
  100. {
  101. category: "artists",
  102. issues: [
  103. {
  104. enabled: false,
  105. title: "Incorrect",
  106. description: "",
  107. showDescription: false
  108. },
  109. {
  110. enabled: false,
  111. title: "Inappropriate",
  112. description: "",
  113. showDescription: false
  114. }
  115. ]
  116. },
  117. {
  118. category: "thumbnail",
  119. issues: [
  120. {
  121. enabled: false,
  122. title: "Incorrect",
  123. description: "",
  124. showDescription: false
  125. },
  126. {
  127. enabled: false,
  128. title: "Inappropriate",
  129. description: "",
  130. showDescription: false
  131. },
  132. {
  133. enabled: false,
  134. title: "Doesn't exist",
  135. description: "",
  136. showDescription: false
  137. }
  138. ]
  139. }
  140. ]);
  141. const create = () => {
  142. const issues = [];
  143. // any predefined issues that are enabled
  144. predefinedCategories.value.forEach(category =>
  145. category.issues.forEach(issue => {
  146. if (issue.enabled)
  147. issues.push({
  148. category: category.category,
  149. title: issue.title,
  150. description: issue.description
  151. });
  152. })
  153. );
  154. // any custom issues
  155. customIssues.value.forEach(issue =>
  156. issues.push({ category: "custom", title: issue })
  157. );
  158. if (issues.length === 0)
  159. return new Toast("Reports must have at least one issue");
  160. return socket.dispatch(
  161. "reports.create",
  162. {
  163. issues,
  164. youtubeId: song.value.youtubeId
  165. },
  166. res => {
  167. new Toast(res.message);
  168. if (res.status === "success") closeCurrentModal();
  169. }
  170. );
  171. };
  172. onMounted(() => {
  173. socket.onConnect(() => {
  174. socket.dispatch("reports.myReportsForSong", song.value._id, res => {
  175. if (res.status === "success") {
  176. existingReports.value = res.data.reports;
  177. existingReports.value.forEach(report =>
  178. socket.dispatch(
  179. "apis.joinRoom",
  180. `view-report.${report._id}`
  181. )
  182. );
  183. }
  184. });
  185. socket.on(
  186. "event:admin.report.resolved",
  187. res => {
  188. existingReports.value = existingReports.value.filter(
  189. report => report._id !== res.data.reportId
  190. );
  191. },
  192. { modalUuid: props.modalUuid }
  193. );
  194. });
  195. });
  196. onBeforeUnmount(() => {
  197. // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
  198. reportStore.$dispose();
  199. });
  200. </script>
  201. <template>
  202. <div>
  203. <modal
  204. class="report-modal"
  205. title="Report"
  206. :size="existingReports.length > 0 ? 'wide' : null"
  207. >
  208. <template #body>
  209. <div class="report-modal-inner-container">
  210. <div id="left-part">
  211. <song-item
  212. :song="song"
  213. :duration="false"
  214. :disabled-actions="['report']"
  215. header="Selected Song.."
  216. />
  217. <div class="columns is-multiline">
  218. <div
  219. v-for="category in predefinedCategories"
  220. class="column is-half"
  221. :key="category.category"
  222. >
  223. <label class="label">{{
  224. category.category
  225. }}</label>
  226. <p
  227. v-for="issue in category.issues"
  228. class="control checkbox-control"
  229. :key="issue.title"
  230. >
  231. <span class="align-horizontally">
  232. <span>
  233. <label class="switch">
  234. <input
  235. type="checkbox"
  236. :id="issue.title"
  237. v-model="issue.enabled"
  238. />
  239. <span
  240. class="slider round"
  241. ></span>
  242. </label>
  243. <label :for="issue.title">
  244. <span></span>
  245. <p>{{ issue.title }}</p>
  246. </label>
  247. </span>
  248. <i
  249. class="material-icons"
  250. content="Provide More info"
  251. v-tippy
  252. @click="
  253. issue.showDescription =
  254. !issue.showDescription
  255. "
  256. >
  257. info
  258. </i>
  259. </span>
  260. <input
  261. type="text"
  262. class="input"
  263. v-model="issue.description"
  264. v-if="issue.showDescription"
  265. placeholder="Provide more information..."
  266. @keyup="issue.enabled = true"
  267. />
  268. </p>
  269. </div>
  270. <!-- allow for multiple custom issues with plus/add button and then a input textbox -->
  271. <!-- do away with textbox -->
  272. <div class="column is-half">
  273. <div id="custom-issues">
  274. <div id="custom-issues-title">
  275. <label class="label"
  276. >Issues not listed</label
  277. >
  278. <button
  279. class="button tab-actionable-button"
  280. content="Add an issue that isn't listed"
  281. v-tippy
  282. @click="customIssues.push('')"
  283. >
  284. <i
  285. class="material-icons icon-with-button"
  286. >add</i
  287. >
  288. <span> Add Custom Issue </span>
  289. </button>
  290. </div>
  291. <div
  292. class="custom-issue control is-grouped input-with-button"
  293. v-for="(issue, index) in customIssues"
  294. :key="index"
  295. >
  296. <p class="control is-expanded">
  297. <input
  298. type="text"
  299. class="input"
  300. v-model="customIssues[index]"
  301. placeholder="Provide information..."
  302. />
  303. </p>
  304. <p class="control">
  305. <button
  306. class="button is-danger"
  307. content="Remove custom issue"
  308. v-tippy
  309. @click="
  310. customIssues.splice(
  311. index,
  312. 1
  313. )
  314. "
  315. >
  316. <i class="material-icons">
  317. delete
  318. </i>
  319. </button>
  320. </p>
  321. </div>
  322. <p
  323. id="no-issues-listed"
  324. v-if="customIssues.length <= 0"
  325. >
  326. <em>
  327. Add any issues that aren't listed
  328. above.
  329. </em>
  330. </p>
  331. </div>
  332. </div>
  333. </div>
  334. </div>
  335. <div id="right-part" v-if="existingReports.length > 0">
  336. <h4 class="section-title">Previous Reports</h4>
  337. <p class="section-description">
  338. You have made
  339. {{
  340. existingReports.length > 1
  341. ? "multiple reports"
  342. : "a report"
  343. }}
  344. about this song already
  345. </p>
  346. <hr class="section-horizontal-rule" />
  347. <div class="report-items">
  348. <div
  349. class="report-item"
  350. v-for="report in existingReports"
  351. :key="report._id"
  352. >
  353. <report-info-item
  354. :created-at="report.createdAt"
  355. :created-by="report.createdBy"
  356. >
  357. <template #actions>
  358. <i
  359. class="material-icons"
  360. content="View Report"
  361. v-tippy
  362. @click="
  363. openModal({
  364. modal: 'viewReport',
  365. data: {
  366. reportId: report._id
  367. }
  368. })
  369. "
  370. >
  371. open_in_full
  372. </i>
  373. </template>
  374. </report-info-item>
  375. </div>
  376. </div>
  377. </div>
  378. </div>
  379. </template>
  380. <template #footer>
  381. <button class="button is-success" @click="create()">
  382. <i class="material-icons save-changes">done</i>
  383. <span>&nbsp;Create</span>
  384. </button>
  385. <a class="button is-danger" @click="closeCurrentModal()">
  386. <span>&nbsp;Cancel</span>
  387. </a>
  388. </template>
  389. </modal>
  390. </div>
  391. </template>
  392. <style lang="less">
  393. .report-modal .song-item .thumbnail {
  394. min-width: 130px;
  395. width: 130px;
  396. height: 130px;
  397. }
  398. </style>
  399. <style lang="less" scoped>
  400. .night-mode {
  401. @media screen and (max-width: 900px) {
  402. #right-part {
  403. background-color: var(--dark-grey-3) !important;
  404. }
  405. }
  406. .columns {
  407. background-color: var(--dark-grey-3) !important;
  408. border-radius: @border-radius;
  409. }
  410. }
  411. .report-modal-inner-container {
  412. display: flex;
  413. @media screen and (max-width: 900px) {
  414. flex-wrap: wrap-reverse;
  415. #left-part {
  416. width: 100%;
  417. }
  418. #right-part {
  419. border-left: 0 !important;
  420. margin-left: 0 !important;
  421. width: 100%;
  422. min-width: 0 !important;
  423. margin-bottom: 20px;
  424. padding: 20px;
  425. background-color: var(--light-grey);
  426. border-radius: @border-radius;
  427. }
  428. }
  429. #right-part {
  430. border-left: 1px solid var(--light-grey-3);
  431. padding-left: 20px;
  432. margin-left: 20px;
  433. min-width: 325px;
  434. .report-items {
  435. max-height: 485px;
  436. overflow: auto;
  437. .report-item:not(:first-of-type) {
  438. margin-top: 10px;
  439. }
  440. }
  441. }
  442. }
  443. .label {
  444. text-transform: capitalize;
  445. }
  446. .columns {
  447. display: flex;
  448. flex-wrap: wrap;
  449. margin-left: unset;
  450. margin-right: unset;
  451. margin-top: 20px;
  452. .column {
  453. flex-basis: 50%;
  454. @media screen and (max-width: 900px) {
  455. flex-basis: 100% !important;
  456. }
  457. }
  458. .control {
  459. display: flex;
  460. flex-direction: column;
  461. span.align-horizontally {
  462. width: 100%;
  463. display: flex;
  464. align-items: center;
  465. justify-content: space-between;
  466. span {
  467. display: flex;
  468. }
  469. }
  470. i {
  471. cursor: pointer;
  472. }
  473. input[type="text"] {
  474. height: initial;
  475. margin: 10px 0;
  476. }
  477. }
  478. }
  479. #custom-issues {
  480. height: 100%;
  481. #custom-issues-title {
  482. display: flex;
  483. align-items: center;
  484. justify-content: space-between;
  485. margin-bottom: 15px;
  486. button {
  487. padding: 3px 5px;
  488. height: initial;
  489. }
  490. label {
  491. margin: 0;
  492. }
  493. }
  494. #no-issues-listed {
  495. display: flex;
  496. height: calc(100% - 32px - 15px);
  497. align-items: center;
  498. justify-content: center;
  499. }
  500. .custom-issue {
  501. flex-direction: row;
  502. input {
  503. height: 36px;
  504. margin: 0;
  505. }
  506. }
  507. }
  508. </style>