SongItem.vue 5.9 KB

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