index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. <template>
  2. <modal
  3. :title="
  4. userId === playlist.createdBy ? 'Edit Playlist' : 'View Playlist'
  5. "
  6. class="edit-playlist-modal"
  7. >
  8. <div
  9. slot="body"
  10. :class="{
  11. 'view-only': !isEditable(),
  12. 'edit-playlist-modal-inner-container': true
  13. }"
  14. >
  15. <div id="first-column">
  16. <div id="playlist-info-section" class="section">
  17. <h3>{{ playlist.displayName }}</h3>
  18. <h5>Duration: {{ totalLength() }}</h5>
  19. </div>
  20. <div
  21. id="playlist-settings-section"
  22. v-if="userId === playlist.createdBy || isEditable()"
  23. class="section"
  24. >
  25. <div v-if="isEditable()">
  26. <h4 class="section-title">Edit Details</h4>
  27. <p class="section-description">
  28. Change the display name and privacy of the playlist.
  29. </p>
  30. <hr class="section-horizontal-rule" />
  31. <label class="label"> Change display name </label>
  32. <div class="control is-grouped input-with-button">
  33. <p class="control is-expanded">
  34. <input
  35. v-model="playlist.displayName"
  36. class="input"
  37. type="text"
  38. placeholder="Playlist Display Name"
  39. @keyup.enter="renamePlaylist()"
  40. />
  41. </p>
  42. <p class="control">
  43. <a
  44. class="button is-info"
  45. @click.prevent="renamePlaylist()"
  46. href="#"
  47. >Rename</a
  48. >
  49. </p>
  50. </div>
  51. </div>
  52. <div>
  53. <label class="label"> Change privacy </label>
  54. <div class="control is-grouped input-with-button">
  55. <div class="control is-expanded select">
  56. <select v-model="playlist.privacy">
  57. <option value="private">Private</option>
  58. <option value="public">Public</option>
  59. </select>
  60. </div>
  61. <p class="control">
  62. <a
  63. class="button is-info"
  64. @click.prevent="updatePrivacy()"
  65. href="#"
  66. >Update Privacy</a
  67. >
  68. </p>
  69. </div>
  70. </div>
  71. </div>
  72. <div
  73. id="import-from-youtube-section"
  74. class="section"
  75. v-if="isEditable()"
  76. >
  77. <h4 class="section-title">Import from YouTube</h4>
  78. <p class="section-description">
  79. Import a playlist or song by searching or using a link
  80. from YouTube.
  81. </p>
  82. <hr class="section-horizontal-rule" />
  83. <label class="label">
  84. Search for a playlist from YouTube
  85. </label>
  86. <div class="control is-grouped input-with-button">
  87. <p class="control is-expanded">
  88. <input
  89. class="input"
  90. type="text"
  91. placeholder="Enter YouTube Playlist URL here..."
  92. v-model="search.playlist.query"
  93. @keyup.enter="importPlaylist()"
  94. />
  95. </p>
  96. <p class="control has-addons">
  97. <span class="select" id="playlist-import-type">
  98. <select
  99. v-model="
  100. search.playlist.isImportingOnlyMusic
  101. "
  102. >
  103. <option :value="false">Import all</option>
  104. <option :value="true">
  105. Import only music
  106. </option>
  107. </select>
  108. </span>
  109. <a
  110. class="button is-info"
  111. @click.prevent="importPlaylist()"
  112. href="#"
  113. ><i class="material-icons icon-with-button"
  114. >publish</i
  115. >Import</a
  116. >
  117. </p>
  118. </div>
  119. <label class="label">
  120. Search for a song from YouTube
  121. </label>
  122. <div class="control is-grouped input-with-button">
  123. <p class="control is-expanded">
  124. <input
  125. class="input"
  126. type="text"
  127. placeholder="Enter your YouTube query here..."
  128. v-model="search.songs.query"
  129. autofocus
  130. @keyup.enter="searchForSongs()"
  131. />
  132. </p>
  133. <p class="control">
  134. <a
  135. class="button is-info"
  136. @click.prevent="searchForSongs()"
  137. href="#"
  138. ><i class="material-icons icon-with-button"
  139. >search</i
  140. >Search</a
  141. >
  142. </p>
  143. </div>
  144. <div
  145. v-if="search.songs.results.length > 0"
  146. id="song-query-results"
  147. >
  148. <search-query-item
  149. v-for="(result, index) in search.songs.results"
  150. :key="index"
  151. :result="result"
  152. >
  153. <div slot="actions">
  154. <transition
  155. name="search-query-actions"
  156. mode="out-in"
  157. >
  158. <a
  159. class="button is-success"
  160. v-if="result.isAddedToQueue"
  161. href="#"
  162. key="added-to-playlist"
  163. >
  164. <i
  165. class="material-icons icon-with-button"
  166. >done</i
  167. >
  168. Added to playlist
  169. </a>
  170. <a
  171. class="button is-dark"
  172. v-else
  173. @click.prevent="
  174. addSongToPlaylist(result.id, index)
  175. "
  176. href="#"
  177. key="add-to-playlist"
  178. >
  179. <i
  180. class="material-icons icon-with-button"
  181. >add</i
  182. >
  183. Add to playlist
  184. </a>
  185. </transition>
  186. </div>
  187. </search-query-item>
  188. <a
  189. class="button is-default load-more-button"
  190. @click.prevent="loadMoreSongs()"
  191. href="#"
  192. >
  193. Load more...
  194. </a>
  195. </div>
  196. </div>
  197. </div>
  198. <div id="second-column">
  199. <div id="rearrange-songs-section" class="section">
  200. <div v-if="isEditable()">
  201. <h4 class="section-title">Rearrange Songs</h4>
  202. <p class="section-description">
  203. Drag and drop songs to change their order
  204. </p>
  205. <hr class="section-horizontal-rule" />
  206. </div>
  207. <aside class="menu">
  208. <draggable
  209. class="menu-list scrollable-list"
  210. tag="ul"
  211. v-if="playlist.songs.length > 0"
  212. v-model="playlist.songs"
  213. v-bind="dragOptions"
  214. @start="drag = true"
  215. @end="drag = false"
  216. @change="updateSongPositioning"
  217. >
  218. <transition-group
  219. type="transition"
  220. :name="
  221. !drag ? 'draggable-list-transition' : null
  222. "
  223. >
  224. <li
  225. v-for="(song, index) in playlist.songs"
  226. :key="'key-' + index"
  227. >
  228. <playlist-song-item
  229. :song="song"
  230. :class="{
  231. 'item-draggable': isEditable()
  232. }"
  233. >
  234. <div slot="actions">
  235. <i
  236. class="material-icons"
  237. v-if="isEditable() && index > 0"
  238. @click="moveSongToTop(index)"
  239. >vertical_align_top</i
  240. >
  241. <i
  242. v-else
  243. class="material-icons"
  244. style="opacity: 0"
  245. >error</i
  246. >
  247. <i
  248. v-if="
  249. isEditable() &&
  250. playlist.songs.length -
  251. 1 !==
  252. index
  253. "
  254. @click="moveSongToBottom(index)"
  255. class="material-icons"
  256. >vertical_align_bottom</i
  257. >
  258. <i
  259. v-else
  260. class="material-icons"
  261. style="opacity: 0"
  262. >error</i
  263. >
  264. <i
  265. v-if="
  266. !song.simpleSong &&
  267. userRole === 'admin'
  268. "
  269. class="material-icons report-icon"
  270. @click="
  271. reportSongInPlaylist(song)
  272. "
  273. >
  274. flag
  275. </i>
  276. <i
  277. v-if="
  278. !song.simpleSong &&
  279. userRole === 'admin'
  280. "
  281. class="material-icons edit-icon"
  282. @click="
  283. editSongInPlaylist(song)
  284. "
  285. >
  286. edit
  287. </i>
  288. <i
  289. v-if="
  290. userId ===
  291. playlist.createdBy
  292. "
  293. @click="
  294. removeSongFromPlaylist(
  295. song.songId
  296. )
  297. "
  298. class="material-icons delete-icon"
  299. >delete</i
  300. >
  301. </div>
  302. </playlist-song-item>
  303. </li>
  304. </transition-group>
  305. </draggable>
  306. <p v-else class="nothing-here-text">
  307. This playlist doesn't have any songs.
  308. </p>
  309. </aside>
  310. </div>
  311. </div>
  312. <!--
  313. <button
  314. class="button is-info"
  315. @click="shuffle()"
  316. v-if="playlist.isUserModifiable"
  317. >
  318. Shuffle
  319. </button>
  320. <h5>Edit playlist details:</h5>
  321. -->
  322. </div>
  323. <div slot="footer">
  324. <a
  325. class="button is-default"
  326. v-if="
  327. this.userId === this.playlist.createdBy ||
  328. this.playlist.privacy === 'public'
  329. "
  330. @click="downloadPlaylist()"
  331. href="#"
  332. >
  333. Download Playlist
  334. </a>
  335. <a
  336. class="button is-danger"
  337. @click="removePlaylist()"
  338. href="#"
  339. v-if="isEditable()"
  340. >
  341. Remove Playlist
  342. </a>
  343. </div>
  344. </modal>
  345. </template>
  346. <script>
  347. import { mapState, mapGetters, mapActions } from "vuex";
  348. import draggable from "vuedraggable";
  349. import Toast from "toasters";
  350. import SearchYoutube from "../../../mixins/SearchYoutube.vue";
  351. import Modal from "../../Modal.vue";
  352. import SearchQueryItem from "../../ui/SearchQueryItem.vue";
  353. import PlaylistSongItem from "./components/PlaylistSongItem.vue";
  354. import validation from "../../../validation";
  355. import utils from "../../../../js/utils";
  356. export default {
  357. components: { Modal, draggable, SearchQueryItem, PlaylistSongItem },
  358. mixins: [SearchYoutube],
  359. data() {
  360. return {
  361. utils,
  362. drag: false,
  363. apiDomain: "",
  364. playlist: { songs: [] }
  365. };
  366. },
  367. computed: {
  368. ...mapState("user/playlists", {
  369. editing: state => state.editing
  370. }),
  371. ...mapState({
  372. userId: state => state.user.auth.userId,
  373. userRole: state => state.user.auth.role
  374. }),
  375. dragOptions() {
  376. return {
  377. animation: 200,
  378. group: "description",
  379. disabled: !this.isEditable(),
  380. ghostClass: "draggable-list-ghost"
  381. };
  382. },
  383. ...mapGetters({
  384. socket: "websockets/getSocket"
  385. })
  386. },
  387. watch: {
  388. "search.songs.results": function checkIfSongInPlaylist(songs) {
  389. songs.forEach((searchItem, index) =>
  390. this.playlist.songs.find(song => {
  391. if (song.songId === searchItem.id)
  392. this.search.songs.results[index].isAddedToQueue = true;
  393. return song.songId === searchItem.id;
  394. })
  395. );
  396. }
  397. },
  398. mounted() {
  399. this.socket.dispatch("playlists.getPlaylist", this.editing, res => {
  400. if (res.status === "success") {
  401. this.playlist = res.data;
  402. this.playlist.songs.sort((a, b) => a.position - b.position);
  403. this.playlist.oldId = res.data._id;
  404. } else
  405. new Toast({
  406. content: res.message,
  407. timeout: 4000
  408. });
  409. });
  410. this.socket.on("event:playlist.addSong", data => {
  411. if (this.playlist._id === data.playlistId)
  412. this.playlist.songs.push(data.song);
  413. });
  414. this.socket.on("event:playlist.removeSong", data => {
  415. if (this.playlist._id === data.playlistId) {
  416. // remove song from array of playlists
  417. this.playlist.songs.forEach((song, index) => {
  418. if (song.songId === data.songId)
  419. this.playlist.songs.splice(index, 1);
  420. });
  421. // if this song is in search results, mark it available to add to the playlist again
  422. this.search.songs.results.forEach((searchItem, index) => {
  423. if (data.songId === searchItem.id) {
  424. this.search.songs.results[index].isAddedToQueue = false;
  425. }
  426. });
  427. }
  428. });
  429. this.socket.on("event:playlist.updateDisplayName", data => {
  430. if (this.playlist._id === data.playlistId)
  431. this.playlist.displayName = data.displayName;
  432. });
  433. this.socket.on("event:playlist.repositionSongs", data => {
  434. if (this.playlist._id === data.playlistId) {
  435. // for each song that has a new position
  436. data.songsBeingChanged.forEach(changedSong => {
  437. this.playlist.songs.forEach((song, index) => {
  438. // find song locally
  439. if (song.songId === changedSong.songId) {
  440. // change song position attribute
  441. this.playlist.songs[index].position =
  442. changedSong.position;
  443. // reposition in array if needed
  444. if (index !== changedSong.position - 1)
  445. this.playlist.songs.splice(
  446. changedSong.position - 1,
  447. 0,
  448. this.playlist.songs.splice(index, 1)[0]
  449. );
  450. }
  451. });
  452. });
  453. }
  454. });
  455. },
  456. methods: {
  457. editSongInPlaylist(song) {
  458. this.$parent.editingSongId = song._id;
  459. this.openModal({ sector: "admin", modal: "editSong" });
  460. },
  461. reportSongInPlaylist(song) {
  462. this.reportSong(song);
  463. this.openModal({ sector: "station", modal: "report" });
  464. },
  465. importPlaylist() {
  466. let isImportingPlaylist = true;
  467. // import query is blank
  468. if (!this.search.playlist.query)
  469. return new Toast({
  470. content: "Please enter a YouTube playlist URL.",
  471. timeout: 4000
  472. });
  473. // don't give starting import message instantly in case of instant error
  474. setTimeout(() => {
  475. if (isImportingPlaylist) {
  476. new Toast({
  477. content:
  478. "Starting to import your playlist. This can take some time to do.",
  479. timeout: 4000
  480. });
  481. }
  482. }, 750);
  483. return this.socket.dispatch(
  484. "playlists.addSetToPlaylist",
  485. this.search.playlist.query,
  486. this.playlist._id,
  487. this.search.playlist.isImportingOnlyMusic,
  488. res => {
  489. new Toast({ content: res.message, timeout: 20000 });
  490. if (res.status === "success") {
  491. isImportingPlaylist = false;
  492. if (this.search.playlist.isImportingOnlyMusic) {
  493. new Toast({
  494. content: `${res.stats.songsInPlaylistTotal} of the ${res.stats.videosInPlaylistTotal} videos in the playlist were songs.`,
  495. timeout: 20000
  496. });
  497. }
  498. }
  499. }
  500. );
  501. },
  502. isEditable() {
  503. return (
  504. this.playlist.isUserModifiable &&
  505. this.userId === this.playlist.createdBy
  506. );
  507. },
  508. updateSongPositioning({ moved }) {
  509. if (!moved) return; // we only need to update when song is moved
  510. const songsBeingChanged = [];
  511. this.playlist.songs.forEach((song, index) => {
  512. if (song.position !== index + 1)
  513. songsBeingChanged.push({
  514. songId: song.songId,
  515. position: index + 1
  516. });
  517. });
  518. this.socket.dispatch(
  519. "playlists.repositionSongs",
  520. this.playlist._id,
  521. songsBeingChanged,
  522. res => {
  523. new Toast({ content: res.message, timeout: 4000 });
  524. }
  525. );
  526. },
  527. totalLength() {
  528. let length = 0;
  529. this.playlist.songs.forEach(song => {
  530. length += song.duration;
  531. });
  532. return this.utils.formatTimeLong(length);
  533. },
  534. shuffle() {
  535. this.socket.dispatch(
  536. "playlists.shuffle",
  537. this.playlist._id,
  538. res => {
  539. new Toast({ content: res.message, timeout: 4000 });
  540. if (res.status === "success") {
  541. this.playlist.songs = res.data.songs.sort(
  542. (a, b) => a.position - b.position
  543. );
  544. }
  545. }
  546. );
  547. },
  548. addSongToPlaylist(id, index) {
  549. this.socket.dispatch(
  550. "playlists.addSongToPlaylist",
  551. false,
  552. id,
  553. this.playlist._id,
  554. res => {
  555. new Toast({ content: res.message, timeout: 4000 });
  556. if (res.status === "success")
  557. this.search.songs.results[index].isAddedToQueue = true;
  558. }
  559. );
  560. },
  561. removeSongFromPlaylist(id) {
  562. if (this.playlist.displayName === "Liked Songs") {
  563. this.socket.dispatch("songs.unlike", id, res => {
  564. new Toast({ content: res.message, timeout: 4000 });
  565. });
  566. }
  567. if (this.playlist.displayName === "Disliked Songs") {
  568. this.socket.dispatch("songs.undislike", id, res => {
  569. new Toast({ content: res.message, timeout: 4000 });
  570. });
  571. } else {
  572. this.socket.dispatch(
  573. "playlists.removeSongFromPlaylist",
  574. id,
  575. this.playlist._id,
  576. res => {
  577. new Toast({ content: res.message, timeout: 4000 });
  578. }
  579. );
  580. }
  581. },
  582. renamePlaylist() {
  583. const { displayName } = this.playlist;
  584. if (!validation.isLength(displayName, 2, 32))
  585. return new Toast({
  586. content:
  587. "Display name must have between 2 and 32 characters.",
  588. timeout: 8000
  589. });
  590. if (!validation.regex.ascii.test(displayName))
  591. return new Toast({
  592. content:
  593. "Invalid display name format. Only ASCII characters are allowed.",
  594. timeout: 8000
  595. });
  596. return this.socket.dispatch(
  597. "playlists.updateDisplayName",
  598. this.playlist._id,
  599. this.playlist.displayName,
  600. res => {
  601. new Toast({ content: res.message, timeout: 4000 });
  602. }
  603. );
  604. },
  605. removePlaylist() {
  606. this.socket.dispatch("playlists.remove", this.playlist._id, res => {
  607. new Toast({ content: res.message, timeout: 3000 });
  608. if (res.status === "success") {
  609. this.closeModal({
  610. sector: "station",
  611. modal: "editPlaylist"
  612. });
  613. }
  614. });
  615. },
  616. async downloadPlaylist() {
  617. if (this.apiDomain === "")
  618. this.apiDomain = await lofig.get("apiDomain");
  619. fetch(
  620. `${this.apiDomain}/export/privatePlaylist/${this.playlist._id}`,
  621. { credentials: "include" }
  622. )
  623. .then(res => res.blob())
  624. .then(blob => {
  625. const url = window.URL.createObjectURL(blob);
  626. const a = document.createElement("a");
  627. a.style.display = "none";
  628. a.href = url;
  629. a.download = `musare-privateplaylist-${
  630. this.playlist._id
  631. }-${new Date().toISOString()}.json`;
  632. document.body.appendChild(a);
  633. a.click();
  634. window.URL.revokeObjectURL(url);
  635. new Toast({
  636. content: "Successfully downloaded playlist.",
  637. timeout: 3000
  638. });
  639. })
  640. .catch(
  641. () =>
  642. new Toast({
  643. content: "Failed to export and download playlist.",
  644. timeout: 3000
  645. })
  646. );
  647. },
  648. moveSongToTop(index) {
  649. this.playlist.songs.splice(
  650. 0,
  651. 0,
  652. this.playlist.songs.splice(index, 1)[0]
  653. );
  654. this.updateSongPositioning({ moved: {} });
  655. },
  656. moveSongToBottom(index) {
  657. this.playlist.songs.splice(
  658. this.playlist.songs.length,
  659. 0,
  660. this.playlist.songs.splice(index, 1)[0]
  661. );
  662. this.updateSongPositioning({ moved: {} });
  663. },
  664. updatePrivacy() {
  665. const { privacy } = this.playlist;
  666. if (privacy === "public" || privacy === "private") {
  667. this.socket.dispatch(
  668. "playlists.updatePrivacy",
  669. this.playlist._id,
  670. privacy,
  671. res => {
  672. new Toast({ content: res.message, timeout: 4000 });
  673. }
  674. );
  675. }
  676. },
  677. ...mapActions("modals/report", ["reportSong"]),
  678. ...mapActions("modalVisibility", ["openModal", "closeModal"])
  679. }
  680. };
  681. </script>
  682. <style lang="scss">
  683. .edit-playlist-modal {
  684. .modal-card {
  685. width: 1300px;
  686. .modal-card-body {
  687. padding: 16px;
  688. }
  689. }
  690. .modal-card-foot {
  691. justify-content: flex-end;
  692. }
  693. }
  694. </style>
  695. <style lang="scss" scoped>
  696. .night-mode {
  697. .section {
  698. background-color: var(--dark-grey-3) !important;
  699. }
  700. .label,
  701. p,
  702. strong {
  703. color: var(--light-grey-2);
  704. }
  705. }
  706. .menu-list li {
  707. display: flex;
  708. justify-content: space-between;
  709. &:not(:last-of-type) {
  710. margin-bottom: 10px;
  711. }
  712. a {
  713. display: flex;
  714. }
  715. }
  716. .controls {
  717. display: flex;
  718. a {
  719. display: flex;
  720. align-items: center;
  721. }
  722. }
  723. @media screen and (max-width: 1300px) {
  724. .edit-playlist-modal .edit-playlist-modal-inner-container {
  725. height: auto !important;
  726. #import-from-youtube-section #song-query-results,
  727. .section {
  728. max-width: 100% !important;
  729. }
  730. }
  731. }
  732. .edit-playlist-modal {
  733. .edit-playlist-modal-inner-container {
  734. display: flex;
  735. flex-wrap: wrap;
  736. height: 100%;
  737. &.view-only {
  738. height: auto !important;
  739. #first-column {
  740. break-after: always;
  741. }
  742. .section {
  743. max-width: 100% !important;
  744. }
  745. }
  746. }
  747. .nothing-here-text {
  748. display: flex;
  749. align-items: center;
  750. justify-content: center;
  751. }
  752. .section {
  753. padding: 15px !important;
  754. margin: 0 10px;
  755. max-width: 600px;
  756. display: flex;
  757. flex-direction: column;
  758. flex-grow: 1;
  759. }
  760. .label {
  761. font-size: 1rem;
  762. font-weight: normal;
  763. }
  764. .input-with-button .button {
  765. width: 150px;
  766. }
  767. #first-column {
  768. max-width: 100%;
  769. height: 100%;
  770. overflow-y: auto;
  771. flex-grow: 1;
  772. .section {
  773. width: auto;
  774. }
  775. #playlist-info-section {
  776. border: 1px solid var(--light-grey-3);
  777. border-radius: 3px;
  778. padding: 15px !important;
  779. h3 {
  780. font-weight: 600;
  781. font-size: 30px;
  782. }
  783. h5 {
  784. font-size: 18px;
  785. }
  786. h3,
  787. h5 {
  788. margin: 0;
  789. }
  790. }
  791. #import-from-youtube-section {
  792. #playlist-import-type select {
  793. border-radius: 0;
  794. }
  795. #song-query-results {
  796. padding: 10px;
  797. margin-top: 10px;
  798. border: 1px solid var(--light-grey-3);
  799. border-radius: 3px;
  800. max-width: 565px;
  801. .search-query-item:not(:last-of-type) {
  802. margin-bottom: 10px;
  803. }
  804. }
  805. .load-more-button {
  806. width: 100%;
  807. margin-top: 10px;
  808. }
  809. }
  810. }
  811. #second-column {
  812. max-width: 100%;
  813. height: 100%;
  814. overflow-y: auto;
  815. flex-grow: 1;
  816. }
  817. }
  818. </style>