Report.vue 12 KB

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