Report.vue 11 KB

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