SongItem.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. <template>
  2. <div
  3. class="universal-item song-item"
  4. :class="{ 'with-duration': duration }"
  5. v-if="song"
  6. >
  7. <div class="thumbnail-and-info">
  8. <slot v-if="$slots.leftIcon" name="leftIcon" />
  9. <song-thumbnail :song="song" v-if="thumbnail" />
  10. <div class="song-info">
  11. <h6 v-if="header">{{ header }}</h6>
  12. <div class="song-title">
  13. <h4
  14. class="item-title"
  15. :style="
  16. song.artists && song.artists.length < 1
  17. ? { fontSize: '16px' }
  18. : null
  19. "
  20. :title="song.title"
  21. >
  22. {{ song.title }}
  23. </h4>
  24. <i
  25. v-if="song.verified"
  26. class="material-icons verified-song"
  27. content="Verified Song"
  28. v-tippy="{ theme: 'info' }"
  29. >
  30. check_circle
  31. </i>
  32. </div>
  33. <h5
  34. class="item-description"
  35. v-if="formatArtists()"
  36. :title="formatArtists()"
  37. >
  38. {{ formatArtists() }}
  39. </h5>
  40. <p
  41. class="song-request-time"
  42. v-if="requestedBy && song.requestedBy"
  43. >
  44. Requested by
  45. <strong>
  46. <user-id-to-username
  47. :key="song._id"
  48. :user-id="song.requestedBy"
  49. :link="true"
  50. />
  51. {{ formatedRequestedAt }}
  52. ago
  53. </strong>
  54. </p>
  55. </div>
  56. </div>
  57. <div class="duration-and-actions">
  58. <p v-if="duration" class="song-duration">
  59. {{ utils.formatTime(song.duration) }}
  60. </p>
  61. <div
  62. class="universal-item-actions"
  63. v-if="disabledActions.indexOf('all') === -1"
  64. >
  65. <tippy
  66. v-if="loggedIn && hoveredTippy"
  67. :touch="true"
  68. :interactive="true"
  69. placement="left"
  70. theme="songActions"
  71. ref="songActions"
  72. trigger="click"
  73. >
  74. <i
  75. class="material-icons action-dropdown-icon"
  76. content="Song Options"
  77. v-tippy
  78. >more_horiz</i
  79. >
  80. <template #content>
  81. <div class="icons-group">
  82. <a
  83. v-if="disabledActions.indexOf('youtube') === -1"
  84. target="_blank"
  85. :href="`https://www.youtube.com/watch?v=${song.youtubeId}`"
  86. content="View on Youtube"
  87. v-tippy
  88. >
  89. <div class="youtube-icon"></div>
  90. </a>
  91. <i
  92. v-if="disabledActions.indexOf('report') === -1"
  93. class="material-icons report-icon"
  94. @click="report(song)"
  95. content="Report Song"
  96. v-tippy
  97. >
  98. flag
  99. </i>
  100. <add-to-playlist-dropdown
  101. v-if="
  102. disabledActions.indexOf('addToPlaylist') ===
  103. -1
  104. "
  105. :song="song"
  106. placement="top-end"
  107. >
  108. <template #button>
  109. <i
  110. class="
  111. material-icons
  112. add-to-playlist-icon
  113. "
  114. content="Add Song to Playlist"
  115. v-tippy
  116. >playlist_add</i
  117. >
  118. </template>
  119. </add-to-playlist-dropdown>
  120. <i
  121. v-if="
  122. loggedIn &&
  123. userRole === 'admin' &&
  124. disabledActions.indexOf('edit') === -1
  125. "
  126. class="material-icons edit-icon"
  127. @click="edit(song)"
  128. content="Edit Song"
  129. v-tippy
  130. >
  131. edit
  132. </i>
  133. <slot name="tippyActions" />
  134. </div>
  135. </template>
  136. </tippy>
  137. <i
  138. class="material-icons action-dropdown-icon"
  139. v-else-if="loggedIn && !hoveredTippy"
  140. @mouseenter="hoverTippy()"
  141. >more_horiz</i
  142. >
  143. <a
  144. v-else-if="
  145. !loggedIn && disabledActions.indexOf('youtube') === -1
  146. "
  147. target="_blank"
  148. :href="`https://www.youtube.com/watch?v=${song.youtubeId}`"
  149. content="View on Youtube"
  150. v-tippy
  151. >
  152. <div class="youtube-icon"></div>
  153. </a>
  154. </div>
  155. <div class="universal-item-actions" v-if="$slots.actions">
  156. <slot name="actions" />
  157. </div>
  158. </div>
  159. </div>
  160. </template>
  161. <script>
  162. import { mapActions, mapState } from "vuex";
  163. import { formatDistance, parseISO } from "date-fns";
  164. import AddToPlaylistDropdown from "./AddToPlaylistDropdown.vue";
  165. import UserIdToUsername from "./UserIdToUsername.vue";
  166. import SongThumbnail from "./SongThumbnail.vue";
  167. import utils from "../../js/utils";
  168. export default {
  169. components: { UserIdToUsername, AddToPlaylistDropdown, SongThumbnail },
  170. props: {
  171. song: {
  172. type: Object,
  173. default: () => {}
  174. },
  175. requestedBy: {
  176. type: Boolean,
  177. default: false
  178. },
  179. duration: {
  180. type: Boolean,
  181. default: true
  182. },
  183. thumbnail: {
  184. type: Boolean,
  185. default: true
  186. },
  187. disabledActions: {
  188. type: Array,
  189. default: () => []
  190. },
  191. header: {
  192. type: String,
  193. default: null
  194. }
  195. },
  196. data() {
  197. return {
  198. utils,
  199. formatedRequestedAt: null,
  200. formatRequestedAtInterval: null,
  201. hoveredTippy: false
  202. };
  203. },
  204. computed: {
  205. ...mapState({
  206. loggedIn: state => state.user.auth.loggedIn,
  207. userRole: state => state.user.auth.role
  208. })
  209. },
  210. mounted() {
  211. if (this.requestedBy) {
  212. this.formatRequestedAt();
  213. this.formatRequestedAtInterval = setInterval(() => {
  214. this.formatRequestedAt();
  215. }, 30000);
  216. }
  217. },
  218. unmounted() {
  219. clearInterval(this.formatRequestedAtInterval);
  220. },
  221. methods: {
  222. formatRequestedAt() {
  223. if (
  224. this.requestedBy &&
  225. this.song.requestedBy &&
  226. this.song.requestedAt
  227. )
  228. this.formatedRequestedAt = this.formatDistance(
  229. parseISO(this.song.requestedAt),
  230. new Date()
  231. );
  232. },
  233. formatArtists() {
  234. if (this.song.artists.length === 1) {
  235. return this.song.artists[0];
  236. }
  237. if (this.song.artists.length === 2) {
  238. return this.song.artists.join(" & ");
  239. }
  240. if (this.song.artists.length > 2) {
  241. return `${this.song.artists
  242. .slice(0, -1)
  243. .join(", ")} & ${this.song.artists.slice(-1)}`;
  244. }
  245. return null;
  246. },
  247. hideTippyElements() {
  248. this.$refs.songActions.tippy.hide();
  249. setTimeout(
  250. () =>
  251. Array.from(
  252. document.querySelectorAll(".tippy-popper")
  253. ).forEach(popper => popper._tippy.hide()),
  254. 500
  255. );
  256. },
  257. hoverTippy() {
  258. this.hoveredTippy = true;
  259. },
  260. report(song) {
  261. this.hideTippyElements();
  262. this.reportSong(song);
  263. this.openModal("report");
  264. },
  265. edit(song) {
  266. this.hideTippyElements();
  267. this.editSong({ songId: song._id });
  268. this.openModal("editSong");
  269. },
  270. ...mapActions("modals/editSong", ["editSong"]),
  271. ...mapActions("modals/report", ["reportSong"]),
  272. ...mapActions("modalVisibility", ["openModal"]),
  273. formatDistance,
  274. parseISO
  275. }
  276. };
  277. </script>
  278. <style lang="scss" scoped>
  279. .night-mode {
  280. .song-item {
  281. background-color: var(--dark-grey-2) !important;
  282. border: 0 !important;
  283. }
  284. }
  285. /deep/ #nav-dropdown {
  286. margin-top: 36px;
  287. width: 0;
  288. height: 0;
  289. .nav-dropdown-items {
  290. width: 250px;
  291. max-width: 100vw;
  292. position: relative;
  293. right: 175px;
  294. }
  295. }
  296. .song-item {
  297. min-height: 65px;
  298. &:not(:last-of-type) {
  299. margin-bottom: 10px;
  300. }
  301. .thumbnail-and-info,
  302. .duration-and-actions {
  303. display: flex;
  304. align-items: center;
  305. }
  306. .duration-and-actions {
  307. margin-left: 5px;
  308. .universal-item-actions div i {
  309. margin-left: 5px;
  310. }
  311. }
  312. .thumbnail-and-info {
  313. min-width: 0;
  314. }
  315. .thumbnail {
  316. min-width: 65px;
  317. width: 65px;
  318. height: 65px;
  319. margin: -7.5px;
  320. margin-right: calc(20px - 7.5px);
  321. }
  322. .song-info {
  323. display: flex;
  324. flex-direction: column;
  325. justify-content: center;
  326. // margin-left: 20px;
  327. min-width: 0;
  328. *:not(i) {
  329. margin: 0;
  330. font-family: Nunito, Arial, sans-serif;
  331. }
  332. h6 {
  333. color: var(--primary-color) !important;
  334. font-weight: bold;
  335. font-size: 17px;
  336. margin-bottom: 5px;
  337. }
  338. .song-title {
  339. display: flex;
  340. flex-direction: row;
  341. .verified-song {
  342. margin-left: 5px;
  343. }
  344. }
  345. .song-request-time {
  346. font-size: 12px;
  347. margin-top: 7px;
  348. }
  349. }
  350. .song-duration {
  351. font-size: 20px;
  352. }
  353. .edit-icon {
  354. color: var(--primary-color);
  355. }
  356. }
  357. </style>