Show.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. <template>
  2. <div v-if="isUser">
  3. <metadata v-bind:title="`Profile | ${user.username}`" />
  4. <edit-playlist v-if="modals.editPlaylist" />
  5. <create-playlist v-if="modals.createPlaylist" />
  6. <main-header />
  7. <div class="container">
  8. <div class="info-section">
  9. <div class="picture-name-row">
  10. <img
  11. class="profile-picture"
  12. :src="
  13. user.avatar.url && user.avatar.type === 'gravatar'
  14. ? `${user.avatar.url}?d=${notes}&s=250`
  15. : '/assets/notes.png'
  16. "
  17. onerror="this.src='/assets/notes.png'; this.onerror=''"
  18. />
  19. <div>
  20. <div class="name-role-row">
  21. <p class="name">{{ user.name }}</p>
  22. <span
  23. class="role admin"
  24. v-if="user.role === 'admin'"
  25. >admin</span
  26. >
  27. </div>
  28. <p class="username">@{{ user.username }}</p>
  29. </div>
  30. </div>
  31. <div
  32. class="buttons"
  33. v-if="userId === user._id || role === 'admin'"
  34. >
  35. <router-link
  36. :to="`/admin/users?userId=${user._id}`"
  37. class="button is-primary"
  38. v-if="role === 'admin'"
  39. >
  40. Edit
  41. </router-link>
  42. <router-link
  43. to="/settings"
  44. class="button is-primary"
  45. v-if="userId === user._id"
  46. >
  47. Settings
  48. </router-link>
  49. </div>
  50. <div class="bio-row" v-if="user.bio">
  51. <i class="material-icons">notes</i>
  52. <p>{{ user.bio }}</p>
  53. </div>
  54. <div
  55. class="date-location-row"
  56. v-if="user.createdAt || user.location"
  57. >
  58. <div class="date" v-if="user.createdAt">
  59. <i class="material-icons">calendar_today</i>
  60. <p>{{ user.createdAt }}</p>
  61. </div>
  62. <div class="location" v-if="user.location">
  63. <i class="material-icons">location_on</i>
  64. <p>{{ user.location }}</p>
  65. </div>
  66. </div>
  67. </div>
  68. <div class="bottom-section">
  69. <div class="buttons">
  70. <button
  71. :class="{ active: activeTab === 'recentActivity' }"
  72. @click="switchTab('recentActivity')"
  73. >
  74. Recent activity
  75. </button>
  76. <button
  77. :class="{ active: activeTab === 'playlists' }"
  78. @click="switchTab('playlists')"
  79. v-if="user._id === userId"
  80. >
  81. Playlists
  82. </button>
  83. </div>
  84. <div
  85. class="content recent-activity-tab"
  86. v-if="activeTab === 'recentActivity'"
  87. >
  88. <div v-if="activities.length > 0">
  89. <div
  90. class="item activity"
  91. v-for="activity in sortedActivities"
  92. :key="activity._id"
  93. >
  94. <div class="thumbnail">
  95. <img :src="activity.thumbnail" alt="" />
  96. <i class="material-icons activity-type-icon">{{
  97. activity.icon
  98. }}</i>
  99. </div>
  100. <div class="left-part">
  101. <p
  102. class="top-text"
  103. v-html="activity.message"
  104. ></p>
  105. <p class="bottom-text">
  106. {{
  107. formatDistance(
  108. parseISO(activity.createdAt),
  109. new Date(),
  110. { addSuffix: true }
  111. )
  112. }}
  113. </p>
  114. </div>
  115. <div class="actions">
  116. <a
  117. class="hide-icon"
  118. href="#"
  119. @click="hideActivity(activity._id)"
  120. >
  121. <i class="material-icons">visibility_off</i>
  122. </a>
  123. </div>
  124. </div>
  125. </div>
  126. <div v-else>
  127. <h2>No recent activity.</h2>
  128. </div>
  129. </div>
  130. <div
  131. class="content playlists-tab"
  132. v-if="activeTab === 'playlists'"
  133. >
  134. <div
  135. class="item playlist"
  136. v-for="playlist in playlists"
  137. :key="playlist._id"
  138. >
  139. <div class="left-part">
  140. <p class="top-text">{{ playlist.displayName }}</p>
  141. <p class="bottom-text">
  142. {{ totalLength(playlist) }} •
  143. {{ playlist.songs.length }}
  144. {{
  145. playlist.songs.length === 1
  146. ? "song"
  147. : "songs"
  148. }}
  149. </p>
  150. </div>
  151. <div class="actions">
  152. <button
  153. class="button is-primary"
  154. @click="editPlaylistClick(playlist._id)"
  155. >
  156. Edit
  157. </button>
  158. </div>
  159. </div>
  160. <button
  161. class="button is-primary"
  162. @click="
  163. openModal({
  164. sector: 'station',
  165. modal: 'createPlaylist'
  166. })
  167. "
  168. >
  169. Create new playlist
  170. </button>
  171. </div>
  172. </div>
  173. </div>
  174. <main-footer />
  175. </div>
  176. </template>
  177. <script>
  178. import { mapState, mapActions } from "vuex";
  179. import { format, formatDistance, parseISO } from "date-fns";
  180. import Toast from "toasters";
  181. import MainHeader from "../MainHeader.vue";
  182. import MainFooter from "../MainFooter.vue";
  183. import io from "../../io";
  184. import utils from "../../js/utils";
  185. export default {
  186. components: {
  187. MainHeader,
  188. MainFooter,
  189. CreatePlaylist: () => import("../Modals/Playlists/Create.vue"),
  190. EditPlaylist: () => import("../Modals/Playlists/Edit.vue")
  191. },
  192. data() {
  193. return {
  194. utils,
  195. user: {},
  196. notes: "",
  197. isUser: false,
  198. activeTab: "recentActivity",
  199. playlists: [],
  200. activities: []
  201. };
  202. },
  203. computed: {
  204. ...mapState({
  205. role: state => state.user.auth.role,
  206. userId: state => state.user.auth.userId,
  207. ...mapState("modals", {
  208. modals: state => state.modals.station
  209. })
  210. }),
  211. sortedActivities() {
  212. const { activities } = this;
  213. return activities.sort(
  214. (x, y) => new Date(y.createdAt) - new Date(x.createdAt)
  215. );
  216. }
  217. },
  218. mounted() {
  219. lofig.get("frontendDomain").then(frontendDomain => {
  220. this.frontendDomain = frontendDomain;
  221. this.notes = encodeURI(`${this.frontendDomain}/assets/notes.png`);
  222. });
  223. io.getSocket(socket => {
  224. this.socket = socket;
  225. this.socket.emit(
  226. "users.findByUsername",
  227. this.$route.params.username,
  228. res => {
  229. if (res.status === "error") this.$router.go("/404");
  230. else {
  231. this.user = res.data;
  232. this.user.createdAt = format(
  233. parseISO(this.user.createdAt),
  234. "MMMM do yyyy"
  235. );
  236. this.isUser = true;
  237. if (this.user._id === this.userId) {
  238. this.socket.emit("playlists.indexForUser", res => {
  239. if (res.status === "success")
  240. this.playlists = res.data;
  241. });
  242. this.socket.emit(
  243. "activities.getSet",
  244. this.userId,
  245. 1,
  246. res => {
  247. if (res.status === "success") {
  248. for (
  249. let a = 0;
  250. a < res.data.length;
  251. a += 1
  252. ) {
  253. this.formatActivity(
  254. res.data[a],
  255. activity => {
  256. this.activities.unshift(
  257. activity
  258. );
  259. }
  260. );
  261. }
  262. }
  263. }
  264. );
  265. this.socket.on(
  266. "event:activity.create",
  267. activity => {
  268. console.log(activity);
  269. this.formatActivity(activity, activity => {
  270. this.activities.unshift(activity);
  271. });
  272. }
  273. );
  274. this.socket.on(
  275. "event:playlist.create",
  276. playlist => {
  277. this.playlists.push(playlist);
  278. }
  279. );
  280. this.socket.on(
  281. "event:playlist.delete",
  282. playlistId => {
  283. this.playlists.forEach(
  284. (playlist, index) => {
  285. if (playlist._id === playlistId) {
  286. this.playlists.splice(index, 1);
  287. }
  288. }
  289. );
  290. }
  291. );
  292. this.socket.on("event:playlist.addSong", data => {
  293. this.playlists.forEach((playlist, index) => {
  294. if (playlist._id === data.playlistId) {
  295. this.playlists[index].songs.push(
  296. data.song
  297. );
  298. }
  299. });
  300. });
  301. this.socket.on(
  302. "event:playlist.removeSong",
  303. data => {
  304. this.playlists.forEach(
  305. (playlist, index) => {
  306. if (
  307. playlist._id === data.playlistId
  308. ) {
  309. this.playlists[
  310. index
  311. ].songs.forEach(
  312. (song, index2) => {
  313. if (
  314. song._id ===
  315. data.songId
  316. ) {
  317. this.playlists[
  318. index
  319. ].songs.splice(
  320. index2,
  321. 1
  322. );
  323. }
  324. }
  325. );
  326. }
  327. }
  328. );
  329. }
  330. );
  331. this.socket.on(
  332. "event:playlist.updateDisplayName",
  333. data => {
  334. this.playlists.forEach(
  335. (playlist, index) => {
  336. if (
  337. playlist._id === data.playlistId
  338. ) {
  339. this.playlists[
  340. index
  341. ].displayName =
  342. data.displayName;
  343. }
  344. }
  345. );
  346. }
  347. );
  348. }
  349. }
  350. }
  351. );
  352. });
  353. },
  354. methods: {
  355. formatDistance,
  356. parseISO,
  357. switchTab(tabName) {
  358. this.activeTab = tabName;
  359. },
  360. editPlaylistClick(playlistId) {
  361. console.log(playlistId);
  362. this.editPlaylist(playlistId);
  363. this.openModal({ sector: "station", modal: "editPlaylist" });
  364. },
  365. totalLength(playlist) {
  366. let length = 0;
  367. playlist.songs.forEach(song => {
  368. length += song.duration;
  369. });
  370. return this.utils.formatTimeLong(length);
  371. },
  372. hideActivity(activityId) {
  373. this.socket.emit("activities.hideActivity", activityId, res => {
  374. if (res.status === "success") {
  375. this.activities = this.activities.filter(
  376. activity => activity._id !== activityId
  377. );
  378. } else {
  379. new Toast({ content: res.message, timeout: 3000 });
  380. }
  381. });
  382. },
  383. formatActivity(res, cb) {
  384. console.log("activity", res);
  385. const icons = {
  386. created_account: "account_circle",
  387. created_station: "radio",
  388. deleted_station: "delete",
  389. created_playlist: "playlist_add_check",
  390. deleted_playlist: "delete_sweep",
  391. liked_song: "favorite",
  392. added_song_to_playlist: "playlist_add",
  393. added_songs_to_playlist: "playlist_add"
  394. };
  395. const activity = {
  396. ...res,
  397. thumbnail: "",
  398. message: "",
  399. icon: ""
  400. };
  401. const plural = activity.payload.length > 1;
  402. activity.icon = icons[activity.activityType];
  403. if (activity.activityType === "created_account") {
  404. activity.message = "Welcome to Musare!";
  405. return cb(activity);
  406. }
  407. if (activity.activityType === "created_station") {
  408. this.socket.emit(
  409. "stations.getStationForActivity",
  410. activity.payload[0],
  411. res => {
  412. if (res.status === "success") {
  413. activity.message = `Created the station <strong>${res.data.title}</strong>`;
  414. activity.thumbnail = res.data.thumbnail;
  415. return cb(activity);
  416. }
  417. activity.message = "Created a station";
  418. return cb(activity);
  419. }
  420. );
  421. }
  422. if (activity.activityType === "deleted_station") {
  423. activity.message = `Deleted a station`;
  424. return cb(activity);
  425. }
  426. if (activity.activityType === "created_playlist") {
  427. this.socket.emit(
  428. "playlists.getPlaylistForActivity",
  429. activity.payload[0],
  430. res => {
  431. if (res.status === "success") {
  432. activity.message = `Created the playlist <strong>${res.data.title}</strong>`;
  433. // activity.thumbnail = res.data.thumbnail;
  434. return cb(activity);
  435. }
  436. activity.message = "Created a playlist";
  437. return cb(activity);
  438. }
  439. );
  440. }
  441. if (activity.activityType === "deleted_playlist") {
  442. activity.message = `Deleted a playlist`;
  443. return cb(activity);
  444. }
  445. if (activity.activityType === "liked_song") {
  446. if (plural) {
  447. activity.message = `Liked ${activity.payload.length} songs.`;
  448. return cb(activity);
  449. }
  450. this.socket.emit(
  451. "songs.getSongForActivity",
  452. activity.payload[0],
  453. res => {
  454. if (res.status === "success") {
  455. activity.message = `Liked the song <strong>${res.data.title}</strong>`;
  456. activity.thumbnail = res.data.thumbnail;
  457. return cb(activity);
  458. }
  459. activity.message = "Liked a song";
  460. return cb(activity);
  461. }
  462. );
  463. }
  464. if (activity.activityType === "added_song_to_playlist") {
  465. this.socket.emit(
  466. "songs.getSongForActivity",
  467. activity.payload[0].songId,
  468. song => {
  469. console.log(song);
  470. this.socket.emit(
  471. "playlists.getPlaylistForActivity",
  472. activity.payload[0].playlistId,
  473. playlist => {
  474. if (song.status === "success") {
  475. if (playlist.status === "success")
  476. activity.message = `Added the song <strong>${song.data.title}</strong> to the playlist <strong>${playlist.data.title}</strong>`;
  477. else
  478. activity.message = `Added the song <strong>${song.data.title}</strong> to a playlist`;
  479. activity.thumbnail = song.data.thumbnail;
  480. return cb(activity);
  481. }
  482. if (playlist.status === "success") {
  483. activity.message = `Added a song to the playlist <strong>${playlist.data.title}</strong>`;
  484. return cb(activity);
  485. }
  486. activity.message = "Added a song to a playlist";
  487. return cb(activity);
  488. }
  489. );
  490. }
  491. );
  492. }
  493. if (activity.activityType === "added_songs_to_playlist") {
  494. activity.message = `Added ${activity.payload.length} songs to a playlist`;
  495. return cb(activity);
  496. }
  497. return false;
  498. },
  499. ...mapActions("modals", ["openModal"]),
  500. ...mapActions("user/playlists", ["editPlaylist"])
  501. }
  502. };
  503. </script>
  504. <style lang="scss" scoped>
  505. @import "styles/global.scss";
  506. .info-section {
  507. width: 912px;
  508. margin-left: auto;
  509. margin-right: auto;
  510. margin-top: 32px;
  511. padding: 24px;
  512. .picture-name-row {
  513. display: flex;
  514. flex-direction: row;
  515. align-items: center;
  516. justify-content: center;
  517. margin-bottom: 24px;
  518. }
  519. .profile-picture {
  520. width: 100px;
  521. height: 100px;
  522. border-radius: 100%;
  523. margin-right: 32px;
  524. }
  525. .name-role-row {
  526. display: flex;
  527. flex-direction: row;
  528. align-items: center;
  529. }
  530. .name {
  531. font-size: 34px;
  532. line-height: 40px;
  533. color: $dark-grey-3;
  534. }
  535. .role {
  536. padding: 2px 24px;
  537. color: $white;
  538. text-transform: uppercase;
  539. font-size: 12px;
  540. line-height: 14px;
  541. height: 18px;
  542. border-radius: 5px;
  543. margin-left: 12px;
  544. &.admin {
  545. background-color: $red;
  546. }
  547. }
  548. .username {
  549. font-size: 24px;
  550. line-height: 28px;
  551. color: $dark-grey;
  552. }
  553. .buttons {
  554. width: 388px;
  555. display: flex;
  556. flex-direction: row;
  557. margin-left: auto;
  558. margin-right: auto;
  559. margin-bottom: 24px;
  560. .button {
  561. flex: 1;
  562. font-size: 17px;
  563. line-height: 20px;
  564. &:nth-child(2) {
  565. margin-left: 20px;
  566. }
  567. }
  568. }
  569. .bio-row,
  570. .date-location-row {
  571. i {
  572. font-size: 24px;
  573. color: $dark-grey-2;
  574. margin-right: 12px;
  575. }
  576. p {
  577. font-size: 17px;
  578. line-height: 20px;
  579. color: $dark-grey-2;
  580. word-break: break-word;
  581. }
  582. }
  583. .bio-row {
  584. max-width: 608px;
  585. margin-bottom: 24px;
  586. margin-left: auto;
  587. margin-right: auto;
  588. display: flex;
  589. width: max-content;
  590. }
  591. .date-location-row {
  592. max-width: 608px;
  593. margin-left: auto;
  594. margin-right: auto;
  595. margin-bottom: 24px;
  596. display: flex;
  597. width: max-content;
  598. margin-bottom: 24px;
  599. > div:nth-child(2) {
  600. margin-left: 48px;
  601. }
  602. }
  603. .date,
  604. .location {
  605. display: flex;
  606. }
  607. }
  608. .bottom-section {
  609. width: 962px;
  610. margin-left: auto;
  611. margin-right: auto;
  612. margin-top: 32px;
  613. padding: 24px;
  614. display: flex;
  615. .buttons {
  616. height: 100%;
  617. width: 250px;
  618. margin-right: 64px;
  619. button {
  620. outline: none;
  621. border: none;
  622. box-shadow: none;
  623. color: $musareBlue;
  624. font-size: 22px;
  625. line-height: 26px;
  626. padding: 7px 0 7px 12px;
  627. width: 100%;
  628. text-align: left;
  629. cursor: pointer;
  630. border-radius: 5px;
  631. background-color: transparent;
  632. &.active {
  633. color: $white;
  634. background-color: $musareBlue;
  635. }
  636. }
  637. }
  638. .content {
  639. width: 600px;
  640. .item {
  641. width: 100%;
  642. height: 72px;
  643. border: 0.5px $light-grey-2 solid;
  644. margin-bottom: 12px;
  645. border-radius: 0 5px 5px 0;
  646. display: flex;
  647. .top-text {
  648. color: $dark-grey-2;
  649. font-size: 20px;
  650. line-height: 23px;
  651. margin-bottom: 0;
  652. }
  653. .bottom-text {
  654. color: $dark-grey-2;
  655. font-size: 16px;
  656. line-height: 19px;
  657. margin-bottom: 0;
  658. margin-top: 6px;
  659. &:first-letter {
  660. text-transform: uppercase;
  661. }
  662. }
  663. .thumbnail {
  664. position: relative;
  665. display: flex;
  666. align-items: center;
  667. justify-content: center;
  668. width: 70.5px;
  669. height: 70.5px;
  670. background-color: #000;
  671. img {
  672. opacity: 0.4;
  673. }
  674. .activity-type-icon {
  675. position: absolute;
  676. color: #fff;
  677. }
  678. }
  679. .left-part {
  680. flex: 1;
  681. padding: 12px;
  682. }
  683. .actions {
  684. display: flex;
  685. align-items: center;
  686. padding: 12px;
  687. .hide-icon {
  688. border-bottom: 0;
  689. display: flex;
  690. i {
  691. color: #bdbdbd;
  692. }
  693. }
  694. }
  695. button {
  696. font-size: 17px;
  697. }
  698. }
  699. }
  700. .playlists-tab > button {
  701. width: 100%;
  702. font-size: 17px;
  703. }
  704. }
  705. .night-mode {
  706. .name,
  707. .username,
  708. .bio-row i,
  709. .bio-row p,
  710. .date-location-row i,
  711. .date-location-row p,
  712. .item .left-part .top-text,
  713. .item .left-part .bottom-text {
  714. color: $light-grey;
  715. }
  716. }
  717. </style>