Browse Source

chore(frontend): moved to vue 3.0, still some remaining bugs

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 3 years ago
parent
commit
6833d6bc5e
56 changed files with 2219 additions and 1790 deletions
  1. 377 114
      frontend/package-lock.json
  2. 8 9
      frontend/package.json
  3. 81 73
      frontend/src/App.vue
  4. 32 32
      frontend/src/components/AddToPlaylistDropdown.vue
  5. 10 10
      frontend/src/components/Confirm.vue
  6. 38 40
      frontend/src/components/Queue.vue
  7. 1 0
      frontend/src/components/SaveButton.vue
  8. 54 50
      frontend/src/components/SongItem.vue
  9. 1 4
      frontend/src/components/layout/MainFooter.vue
  10. 2 2
      frontend/src/components/layout/MainHeader.vue
  11. 4 4
      frontend/src/components/modals/EditNews.vue
  12. 2 2
      frontend/src/components/modals/EditPlaylist/Tabs/Youtube.vue
  13. 171 168
      frontend/src/components/modals/EditPlaylist/index.vue
  14. 5 5
      frontend/src/components/modals/EditSong/index.vue
  15. 4 4
      frontend/src/components/modals/EditUser.vue
  16. 393 360
      frontend/src/components/modals/ManageStationKris/Tabs/Playlists.vue
  17. 182 172
      frontend/src/components/modals/ManageStationKris/Tabs/Settings.vue
  18. 16 12
      frontend/src/components/modals/ManageStationKris/Tabs/Songs.vue
  19. 1 1
      frontend/src/components/modals/ManageStationKris/index.vue
  20. 28 24
      frontend/src/components/modals/ManageStationOwen/Tabs/Blacklist.vue
  21. 188 181
      frontend/src/components/modals/ManageStationOwen/Tabs/Playlists.vue
  22. 14 12
      frontend/src/components/modals/ManageStationOwen/Tabs/Search.vue
  23. 185 172
      frontend/src/components/modals/ManageStationOwen/Tabs/Settings.vue
  24. 1 1
      frontend/src/components/modals/ManageStationOwen/index.vue
  25. 4 4
      frontend/src/components/modals/Report.vue
  26. 4 4
      frontend/src/components/modals/RequestSong.vue
  27. 2 3
      frontend/src/components/modals/ViewPunishment.vue
  28. 4 4
      frontend/src/components/modals/ViewReport.vue
  29. 5 4
      frontend/src/components/modals/WhatIsNew.vue
  30. 27 31
      frontend/src/main.js
  31. 4 4
      frontend/src/mixins/ScrollAndFetchHandler.vue
  32. 20 11
      frontend/src/pages/Admin/index.vue
  33. 5 2
      frontend/src/pages/Admin/tabs/HiddenSongs.vue
  34. 4 1
      frontend/src/pages/Admin/tabs/News.vue
  35. 10 3
      frontend/src/pages/Admin/tabs/Playlists.vue
  36. 4 1
      frontend/src/pages/Admin/tabs/Punishments.vue
  37. 4 1
      frontend/src/pages/Admin/tabs/Reports.vue
  38. 22 9
      frontend/src/pages/Admin/tabs/Stations.vue
  39. 5 2
      frontend/src/pages/Admin/tabs/UnverifiedSongs.vue
  40. 4 1
      frontend/src/pages/Admin/tabs/Users.vue
  41. 6 3
      frontend/src/pages/Admin/tabs/VerifiedSongs.vue
  42. 63 59
      frontend/src/pages/Home.vue
  43. 38 39
      frontend/src/pages/Profile/Tabs/Playlists.vue
  44. 4 3
      frontend/src/pages/Profile/Tabs/RecentActivity.vue
  45. 12 5
      frontend/src/pages/Profile/index.vue
  46. 2 2
      frontend/src/pages/ResetPassword.vue
  47. 19 6
      frontend/src/pages/Settings/index.vue
  48. 81 76
      frontend/src/pages/Station/Sidebar/Playlists.vue
  49. 1 2
      frontend/src/pages/Station/Sidebar/index.vue
  50. 59 43
      frontend/src/pages/Station/index.vue
  51. 2 5
      frontend/src/store/index.js
  52. 3 1
      frontend/src/store/modules/admin.js
  53. 0 3
      frontend/src/store/modules/modals/editPlaylist.js
  54. 0 3
      frontend/src/store/modules/modals/editSong.js
  55. 1 1
      frontend/webpack.common.js
  56. 2 2
      frontend/webpack.dev.js

+ 377 - 114
frontend/package-lock.json

@@ -2758,6 +2758,11 @@
       "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz",
       "integrity": "sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA=="
     },
+    "@popperjs/core": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz",
+      "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q=="
+    },
     "@types/color-name": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@@ -2824,22 +2829,164 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz",
       "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag=="
     },
-    "@vue/component-compiler-utils": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz",
-      "integrity": "sha512-lejBLa7xAMsfiZfNp7Kv51zOzifnb29FwdnMLa96z26kXErPFioSf9BMcePVIQ6/Gc6/mC0UrPpxAWIHyae0vw==",
+    "@vue/compiler-core": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.0.11.tgz",
+      "integrity": "sha512-6sFj6TBac1y2cWCvYCA8YzHJEbsVkX7zdRs/3yK/n1ilvRqcn983XvpBbnN3v4mZ1UiQycTvOiajJmOgN9EVgw==",
       "requires": {
-        "consolidate": "^0.15.1",
-        "hash-sum": "^1.0.2",
-        "lru-cache": "^4.1.2",
+        "@babel/parser": "^7.12.0",
+        "@babel/types": "^7.12.0",
+        "@vue/shared": "3.0.11",
+        "estree-walker": "^2.0.1",
+        "source-map": "^0.6.1"
+      },
+      "dependencies": {
+        "@babel/helper-validator-identifier": {
+          "version": "7.14.0",
+          "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+          "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A=="
+        },
+        "@babel/parser": {
+          "version": "7.14.4",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
+          "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA=="
+        },
+        "@babel/types": {
+          "version": "7.14.4",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+          "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
+          "requires": {
+            "@babel/helper-validator-identifier": "^7.14.0",
+            "to-fast-properties": "^2.0.0"
+          }
+        }
+      }
+    },
+    "@vue/compiler-dom": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.11.tgz",
+      "integrity": "sha512-+3xB50uGeY5Fv9eMKVJs2WSRULfgwaTJsy23OIltKgMrynnIj8hTYY2UL97HCoz78aDw1VDXdrBQ4qepWjnQcw==",
+      "requires": {
+        "@vue/compiler-core": "3.0.11",
+        "@vue/shared": "3.0.11"
+      }
+    },
+    "@vue/compiler-sfc": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.0.11.tgz",
+      "integrity": "sha512-7fNiZuCecRleiyVGUWNa6pn8fB2fnuJU+3AGjbjl7r1P5wBivfl02H4pG+2aJP5gh2u+0wXov1W38tfWOphsXw==",
+      "dev": true,
+      "requires": {
+        "@babel/parser": "^7.13.9",
+        "@babel/types": "^7.13.0",
+        "@vue/compiler-core": "3.0.11",
+        "@vue/compiler-dom": "3.0.11",
+        "@vue/compiler-ssr": "3.0.11",
+        "@vue/shared": "3.0.11",
+        "consolidate": "^0.16.0",
+        "estree-walker": "^2.0.1",
+        "hash-sum": "^2.0.0",
+        "lru-cache": "^5.1.1",
+        "magic-string": "^0.25.7",
         "merge-source-map": "^1.1.0",
-        "postcss": "^7.0.14",
-        "postcss-selector-parser": "^6.0.2",
-        "prettier": "^1.18.2",
-        "source-map": "~0.6.1",
-        "vue-template-es2015-compiler": "^1.9.0"
+        "postcss": "^8.1.10",
+        "postcss-modules": "^4.0.0",
+        "postcss-selector-parser": "^6.0.4",
+        "source-map": "^0.6.1"
+      },
+      "dependencies": {
+        "@babel/helper-validator-identifier": {
+          "version": "7.14.0",
+          "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
+          "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+          "dev": true
+        },
+        "@babel/parser": {
+          "version": "7.14.4",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
+          "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==",
+          "dev": true
+        },
+        "@babel/types": {
+          "version": "7.14.4",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
+          "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
+          "dev": true,
+          "requires": {
+            "@babel/helper-validator-identifier": "^7.14.0",
+            "to-fast-properties": "^2.0.0"
+          }
+        },
+        "hash-sum": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
+          "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
+          "dev": true
+        },
+        "lru-cache": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+          "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+          "dev": true,
+          "requires": {
+            "yallist": "^3.0.2"
+          }
+        },
+        "yallist": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+          "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+          "dev": true
+        }
       }
     },
+    "@vue/compiler-ssr": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.0.11.tgz",
+      "integrity": "sha512-66yUGI8SGOpNvOcrQybRIhl2M03PJ+OrDPm78i7tvVln86MHTKhM3ERbALK26F7tXl0RkjX4sZpucCpiKs3MnA==",
+      "dev": true,
+      "requires": {
+        "@vue/compiler-dom": "3.0.11",
+        "@vue/shared": "3.0.11"
+      }
+    },
+    "@vue/devtools-api": {
+      "version": "6.0.0-beta.12",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.12.tgz",
+      "integrity": "sha512-PtHmAxFmCyCElV7uTWMrXj+fefwn4lCfTtPo9fPw0SK8/7e3UaFl8IL7lnugJmNFfeKQyuTkSoGvTq1uDaRF6Q=="
+    },
+    "@vue/reactivity": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.11.tgz",
+      "integrity": "sha512-SKM3YKxtXHBPMf7yufXeBhCZ4XZDKP9/iXeQSC8bBO3ivBuzAi4aZi0bNoeE2IF2iGfP/AHEt1OU4ARj4ao/Xw==",
+      "requires": {
+        "@vue/shared": "3.0.11"
+      }
+    },
+    "@vue/runtime-core": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.0.11.tgz",
+      "integrity": "sha512-87XPNwHfz9JkmOlayBeCCfMh9PT2NBnv795DSbi//C/RaAnc/bGZgECjmkD7oXJ526BZbgk9QZBPdFT8KMxkAg==",
+      "requires": {
+        "@vue/reactivity": "3.0.11",
+        "@vue/shared": "3.0.11"
+      }
+    },
+    "@vue/runtime-dom": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.11.tgz",
+      "integrity": "sha512-jm3FVQESY3y2hKZ2wlkcmFDDyqaPyU3p1IdAX92zTNeCH7I8zZ37PtlE1b9NlCtzV53WjB4TZAYh9yDCMIEumA==",
+      "requires": {
+        "@vue/runtime-core": "3.0.11",
+        "@vue/shared": "3.0.11",
+        "csstype": "^2.6.8"
+      }
+    },
+    "@vue/shared": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz",
+      "integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA=="
+    },
     "@webassemblyjs/ast": {
       "version": "1.11.0",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz",
@@ -3103,6 +3250,7 @@
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
       "requires": {
         "color-convert": "^1.9.0"
       }
@@ -3524,11 +3672,6 @@
         "resolve": "^1.12.0"
       }
     },
-    "babel-helper-vue-jsx-merge-props": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
-      "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg=="
-    },
     "babel-loader": {
       "version": "8.2.2",
       "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz",
@@ -3706,7 +3849,8 @@
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
-      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+      "dev": true
     },
     "body-parser": {
       "version": "1.19.0",
@@ -3970,6 +4114,7 @@
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dev": true,
       "requires": {
         "ansi-styles": "^3.2.1",
         "escape-string-regexp": "^1.0.5",
@@ -3980,6 +4125,7 @@
           "version": "5.5.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
           "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
           }
@@ -4670,6 +4816,7 @@
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
       "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
       "requires": {
         "color-name": "1.1.3"
       },
@@ -4677,7 +4824,8 @@
         "color-name": {
           "version": "1.1.3",
           "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+          "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+          "dev": true
         }
       }
     },
@@ -4802,11 +4950,12 @@
       "dev": true
     },
     "consolidate": {
-      "version": "0.15.1",
-      "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz",
-      "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==",
+      "version": "0.16.0",
+      "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz",
+      "integrity": "sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==",
+      "dev": true,
       "requires": {
-        "bluebird": "^3.1.1"
+        "bluebird": "^3.7.2"
       }
     },
     "contains-path": {
@@ -5031,7 +5180,13 @@
     "cssesc": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
-      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true
+    },
+    "csstype": {
+      "version": "2.6.17",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz",
+      "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A=="
     },
     "currently-unhandled": {
       "version": "0.4.1",
@@ -5056,12 +5211,6 @@
       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.19.0.tgz",
       "integrity": "sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg=="
     },
-    "de-indent": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
-      "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
-      "dev": true
-    },
     "debug": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -5473,7 +5622,8 @@
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
     },
     "eslint": {
       "version": "6.8.0",
@@ -5901,6 +6051,11 @@
       "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
       "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
     },
+    "estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
     "esutils": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -6484,6 +6639,15 @@
         "globule": "^1.0.0"
       }
     },
+    "generic-names": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz",
+      "integrity": "sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0"
+      }
+    },
     "gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -6664,7 +6828,8 @@
     "has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "dev": true
     },
     "has-symbols": {
       "version": "1.0.1",
@@ -6712,7 +6877,8 @@
     "hash-sum": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
-      "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ="
+      "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
+      "dev": true
     },
     "he": {
       "version": "1.2.0",
@@ -6893,11 +7059,6 @@
       "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
       "dev": true
     },
-    "humps": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz",
-      "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao="
-    },
     "iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -6907,6 +7068,12 @@
         "safer-buffer": ">= 2.1.2 < 3"
       }
     },
+    "icss-replace-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
+      "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
+      "dev": true
+    },
     "icss-utils": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
@@ -6963,7 +7130,8 @@
     "indexes-of": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
-      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
+      "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
+      "dev": true
     },
     "inflight": {
       "version": "1.0.6",
@@ -7608,6 +7776,7 @@
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
       "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
+      "dev": true,
       "requires": {
         "big.js": "^5.2.2",
         "emojis-list": "^3.0.0",
@@ -7618,6 +7787,7 @@
           "version": "1.0.1",
           "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
           "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+          "dev": true,
           "requires": {
             "minimist": "^1.2.0"
           }
@@ -7638,6 +7808,12 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
     },
+    "lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
+      "dev": true
+    },
     "lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -7679,11 +7855,21 @@
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
       "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+      "dev": true,
       "requires": {
         "pseudomap": "^1.0.2",
         "yallist": "^2.1.2"
       }
     },
+    "magic-string": {
+      "version": "0.25.7",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
+      "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
+      "dev": true,
+      "requires": {
+        "sourcemap-codec": "^1.4.4"
+      }
+    },
     "make-dir": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -7884,6 +8070,7 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
       "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==",
+      "dev": true,
       "requires": {
         "source-map": "^0.6.1"
       }
@@ -8815,11 +9002,6 @@
         }
       }
     },
-    "popper.js": {
-      "version": "1.16.1",
-      "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
-      "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
-    },
     "portfinder": {
       "version": "1.0.28",
       "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@@ -8849,13 +9031,30 @@
       "dev": true
     },
     "postcss": {
-      "version": "7.0.35",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
-      "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz",
+      "integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==",
+      "dev": true,
       "requires": {
-        "chalk": "^2.4.2",
-        "source-map": "^0.6.1",
-        "supports-color": "^6.1.0"
+        "colorette": "^1.2.2",
+        "nanoid": "^3.1.23",
+        "source-map-js": "^0.6.2"
+      }
+    },
+    "postcss-modules": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.1.3.tgz",
+      "integrity": "sha512-dBT39hrXe4OAVYJe/2ZuIZ9BzYhOe7t+IhedYeQ2OxKwDpAGlkEN/fR0fGnrbx4BvgbMReRX4hCubYK9cE/pJQ==",
+      "dev": true,
+      "requires": {
+        "generic-names": "^2.0.1",
+        "icss-replace-symbols": "^1.1.0",
+        "lodash.camelcase": "^4.3.0",
+        "postcss-modules-extract-imports": "^3.0.0",
+        "postcss-modules-local-by-default": "^4.0.0",
+        "postcss-modules-scope": "^3.0.0",
+        "postcss-modules-values": "^4.0.0",
+        "string-hash": "^1.1.1"
       }
     },
     "postcss-modules-extract-imports": {
@@ -8897,6 +9096,7 @@
       "version": "6.0.4",
       "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz",
       "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==",
+      "dev": true,
       "requires": {
         "cssesc": "^3.0.0",
         "indexes-of": "^1.0.1",
@@ -8919,7 +9119,8 @@
     "prettier": {
       "version": "1.18.2",
       "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
-      "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw=="
+      "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
+      "dev": true
     },
     "prettier-linter-helpers": {
       "version": "1.0.0",
@@ -8970,7 +9171,8 @@
     "pseudomap": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
-      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
     },
     "psl": {
       "version": "1.8.0",
@@ -9991,6 +10193,12 @@
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
     },
+    "source-map-js": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
+      "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==",
+      "dev": true
+    },
     "source-map-resolve": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
@@ -10019,6 +10227,12 @@
       "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
       "dev": true
     },
+    "sourcemap-codec": {
+      "version": "1.4.8",
+      "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+      "dev": true
+    },
     "spdx-correct": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
@@ -10178,6 +10392,12 @@
         }
       }
     },
+    "string-hash": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
+      "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=",
+      "dev": true
+    },
     "string-width": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
@@ -10305,6 +10525,7 @@
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
       "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+      "dev": true,
       "requires": {
         "has-flag": "^3.0.0"
       }
@@ -10484,11 +10705,11 @@
       "dev": true
     },
     "tippy.js": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-4.3.5.tgz",
-      "integrity": "sha512-NDq3efte8nGK6BOJ1dDN1/WelAwfmh3UtIYXXck6+SxLzbIQNZE/cmRSnwScZ/FyiKdIcvFHvYUgqmoGx8CcyA==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.1.tgz",
+      "integrity": "sha512-JnFncCq+rF1dTURupoJ4yPie5Cof978inW6/4S6kmWV7LL9YOSEVMifED3KdrVPEG+Z/TFH2CDNJcQEfaeuQww==",
       "requires": {
-        "popper.js": "^1.14.7"
+        "@popperjs/core": "^2.8.3"
       }
     },
     "tmp": {
@@ -10503,8 +10724,7 @@
     "to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
-      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
-      "dev": true
+      "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
     },
     "to-object-path": {
       "version": "0.3.0",
@@ -10712,7 +10932,8 @@
     "uniq": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
-      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8="
+      "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
+      "dev": true
     },
     "unpipe": {
       "version": "1.0.0",
@@ -10870,18 +11091,20 @@
       }
     },
     "vue": {
-      "version": "2.6.12",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
-      "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
-    },
-    "vue-content-loader": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-0.2.3.tgz",
-      "integrity": "sha512-gJlNEdXkuHGvgnyY0lBMsrSsOMk+TTog5uNAil5MSLv08f/mE7Apag0VavpxJ6ieb4P5J1iVKEIhHI41HQNq9Q==",
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.0.11.tgz",
+      "integrity": "sha512-3/eUi4InQz8MPzruHYSTQPxtM3LdZ1/S/BvaU021zBnZi0laRUyH6pfuE4wtUeLvI8wmUNwj5wrZFvbHUXL9dw==",
       "requires": {
-        "babel-helper-vue-jsx-merge-props": "^2.0.3"
+        "@vue/compiler-dom": "3.0.11",
+        "@vue/runtime-dom": "3.0.11",
+        "@vue/shared": "3.0.11"
       }
     },
+    "vue-content-loader": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-2.0.0.tgz",
+      "integrity": "sha512-4HtRASF3y1S03pcilwneVZzbowwKyDBfsPzP2JE35RMDIjsPuGnr93nO/2kP4mmqP4Qwmw6uCPd3SHBgJAi8Bw=="
+    },
     "vue-eslint-parser": {
       "version": "7.6.0",
       "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.6.0.tgz",
@@ -10913,73 +11136,112 @@
         }
       }
     },
-    "vue-hot-reload-api": {
-      "version": "2.3.4",
-      "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
-      "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog=="
-    },
     "vue-loader": {
-      "version": "15.9.6",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.6.tgz",
-      "integrity": "sha512-j0cqiLzwbeImIC6nVIby2o/ABAWhlppyL/m5oJ67R5MloP0hj/DtFgb0Zmq3J9CG7AJ+AXIvHVnJAPBvrLyuDg==",
+      "version": "16.2.0",
+      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.2.0.tgz",
+      "integrity": "sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q==",
       "requires": {
-        "@vue/component-compiler-utils": "^3.1.0",
-        "hash-sum": "^1.0.2",
-        "loader-utils": "^1.1.0",
-        "vue-hot-reload-api": "^2.3.0",
-        "vue-style-loader": "^4.1.0"
+        "chalk": "^4.1.0",
+        "hash-sum": "^2.0.0",
+        "loader-utils": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
+          "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+        },
+        "hash-sum": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
+          "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="
+        },
+        "loader-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
+          "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
       }
     },
     "vue-router": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.1.tgz",
-      "integrity": "sha512-RRQNLT8Mzr8z7eL4p7BtKvRaTSGdCbTy2+Mm5HTJvLGYSSeG9gDzNasJPP/yOYKLy+/cLG/ftrqq5fvkFwBJEw=="
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.8.tgz",
+      "integrity": "sha512-42mWSQaH7CCBQDspQTHv63f34VEnZC20g9QNK4WJ/zW8SdIUeT6TQ2i/78fjF/pVBUPLBWrGhvB7uDnaz7O/pA==",
+      "requires": {
+        "@vue/devtools-api": "^6.0.0-beta.10"
+      }
     },
     "vue-style-loader": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
       "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==",
+      "dev": true,
       "requires": {
         "hash-sum": "^1.0.2",
         "loader-utils": "^1.0.2"
       }
     },
-    "vue-template-compiler": {
-      "version": "2.6.12",
-      "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz",
-      "integrity": "sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg==",
-      "dev": true,
-      "requires": {
-        "de-indent": "^1.0.2",
-        "he": "^1.1.0"
-      }
-    },
-    "vue-template-es2015-compiler": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
-      "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw=="
-    },
     "vue-tippy": {
-      "version": "4.10.0",
-      "resolved": "https://registry.npmjs.org/vue-tippy/-/vue-tippy-4.10.0.tgz",
-      "integrity": "sha512-RUlCOKEVhzeuKiwGDIHZuwBvLYlqV5aGpNm7hQAhCr2r+UGzJvCY6YgalVtZ3DskUj0c3L5PVAX/G3rLZ7BOLQ==",
+      "version": "6.0.0-alpha.29",
+      "resolved": "https://registry.npmjs.org/vue-tippy/-/vue-tippy-6.0.0-alpha.29.tgz",
+      "integrity": "sha512-NqSfgjGAlOJF0YZjzL22rSviqGkvvCv7wQepSepXvjPC9UMjV82ke55bXD6TfhSPdjdpeKi6VqFwFzmgw/g0lg==",
       "requires": {
-        "humps": "^2.0.1",
-        "tippy.js": "^4.3.5"
+        "tippy.js": "^6.3.1"
       }
     },
     "vuedraggable": {
-      "version": "2.24.3",
-      "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
-      "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.0.1.tgz",
+      "integrity": "sha512-7qN5jhB1SLfx5P+HCm3JUW+pvgA1bSLgYLSVOeLWBDH9z+zbaEH0OlyZBVMLOxFR+JUHJjwDD0oy7T4r9TEgDA==",
       "requires": {
         "sortablejs": "1.10.2"
       }
     },
     "vuex": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
-      "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw=="
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.1.tgz",
+      "integrity": "sha512-MddakQTAnImDkK1YhEESowKSU5KcjqHH3L1ScPx1lj6NzDDX0FuRBZqJoME5O7/nvj9puudDf6xnWU2w/cqI7g==",
+      "requires": {
+        "@vue/devtools-api": "^6.0.0-beta.11"
+      }
     },
     "watchpack": {
       "version": "2.1.1",
@@ -11161,9 +11423,9 @@
           }
         },
         "ws": {
-          "version": "7.4.4",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
-          "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
+          "version": "7.4.6",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+          "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
         }
       }
     },
@@ -11614,7 +11876,8 @@
     "yallist": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
-      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+      "dev": true
     },
     "yargs": {
       "version": "13.3.2",

+ 8 - 9
frontend/package.json

@@ -22,6 +22,7 @@
     "@babel/plugin-syntax-dynamic-import": "^7.2.0",
     "@babel/plugin-transform-runtime": "^7.13.10",
     "@babel/preset-env": "^7.13.12",
+    "@vue/compiler-sfc": "^3.0.11",
     "babel-eslint": "^10.0.2",
     "babel-loader": "^8.2.2",
     "css-loader": "^5.2.4",
@@ -35,9 +36,7 @@
     "node-sass": "^4.14.1",
     "prettier": "1.18.2",
     "sass-loader": "^7.1.0",
-    "vue-hot-reload-api": "^2.3.3",
     "vue-style-loader": "^4.1.3",
-    "vue-template-compiler": "^2.6.12",
     "webpack-cli": "4.5.0",
     "webpack-dev-server": "^3.11.2"
   },
@@ -50,13 +49,13 @@
     "html-webpack-plugin": "^5.3.1",
     "marked": "^2.0.3",
     "toasters": "^2.3.1",
-    "vue": "^2.6.12",
-    "vue-content-loader": "^0.2.3",
-    "vue-loader": "^15.9.6",
-    "vue-router": "^3.5.1",
-    "vue-tippy": "^4.10.0",
-    "vuedraggable": "^2.24.3",
-    "vuex": "^3.6.2",
+    "vue": "^3.0.11",
+    "vue-content-loader": "^2.0.0",
+    "vue-loader": "^16.2.0",
+    "vue-router": "^4.0.8",
+    "vue-tippy": "^6.0.0-alpha.29",
+    "vuedraggable": "^4.0.1",
+    "vuex": "^4.0.1",
     "webpack": "5.27.2",
     "webpack-bundle-analyzer": "^4.4.0",
     "webpack-merge": "^5.7.3"

+ 81 - 73
frontend/src/App.vue

@@ -17,6 +17,7 @@
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
+import { defineAsyncComponent } from "vue";
 
 import ws from "./ws";
 import aw from "./aw";
@@ -24,10 +25,16 @@ import keyboardShortcuts from "./keyboardShortcuts";
 
 export default {
 	components: {
-		WhatIsNew: () => import("@/components/modals/WhatIsNew.vue"),
-		LoginModal: () => import("@/components/modals/Login.vue"),
-		RegisterModal: () => import("@/components/modals/Register.vue"),
-		Banned: () => import("@/pages/Banned.vue")
+		WhatIsNew: defineAsyncComponent(() =>
+			import("@/components/modals/WhatIsNew.vue")
+		),
+		LoginModal: defineAsyncComponent(() =>
+			import("@/components/modals/Login.vue")
+		),
+		RegisterModal: defineAsyncComponent(() =>
+			import("@/components/modals/Register.vue")
+		),
+		Banned: defineAsyncComponent(() => import("@/pages/Banned.vue"))
 	},
 	replace: false,
 	data() {
@@ -123,7 +130,7 @@ export default {
 
 		this.apiDomain = await lofig.get("apiDomain");
 
-		this.$router.onReady(() => {
+		this.$router.isReady(() => {
 			if (this.$route.query.err) {
 				let { err } = this.$route.query;
 				err = err
@@ -190,6 +197,9 @@ export default {
 </script>
 
 <style lang="scss">
+@import "tippy.js/dist/tippy.css";
+@import "tippy.js/animations/scale.css";
+
 :root {
 	--primary-color: var(--blue);
 	--blue: rgb(2, 166, 242);
@@ -260,7 +270,7 @@ export default {
 		background-color: var(--dark-grey-3) !important;
 	}
 
-	.tippy-tooltip.songActions-theme {
+	.tippy-box[data-theme~="songActions"] {
 		background-color: var(--dark-grey);
 	}
 }
@@ -323,6 +333,7 @@ textarea {
 
 .main-container {
 	height: 100%;
+	min-height: 100vh;
 	display: flex;
 	flex-direction: column;
 
@@ -365,22 +376,22 @@ a {
 	z-index: 10000000;
 }
 
-.tippy-tooltip.dark-theme {
-	font-size: 14px;
-	padding: 5px 10px;
-}
 .night-mode {
-	.tippy-tooltip {
-		&.dark-theme {
-			border: 1px solid var(--light-grey-3);
-			box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
-				0 10px 10px rgba(0, 0, 0, 0.22);
-			background-color: white;
-			.tippy-content {
-				color: var(--black);
-			}
+	.tippy-box {
+		border: 1px solid var(--light-grey-3);
+		box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
+			0 10px 10px rgba(0, 0, 0, 0.22);
+		background-color: var(--white);
+
+		> .tippy-arrow::before {
+			border-top-color: var(--white);
+		}
+
+		.tippy-content {
+			color: var(--black);
 		}
-		&.songActions-theme {
+
+		&[data-theme~="songActions"] {
 			background-color: var(--dark-grey-2);
 			border: 0 !important;
 
@@ -393,7 +404,7 @@ a {
 				background-color: var(--white);
 			}
 		}
-		&.addToPlaylist-theme {
+		&[data-theme~="addToPlaylist"] {
 			background-color: var(--dark-grey-2);
 			border: 0 !important;
 
@@ -413,64 +424,58 @@ a {
 		}
 	}
 
-	.tippy-popper[x-placement^="top"] .tippy-tooltip {
-		&.songActions-theme,
-		&.addToPlaylist-theme {
-			.tippy-arrow {
+	.tippy-box[data-placement^="top"] {
+		&[data-theme~="songActions"],
+		&[data-theme~="addToPlaylist"] {
+			> .tippy-arrow::before {
 				border-top-color: var(--dark-grey-2);
 			}
 		}
-		&.dark-theme .tippy-arrow {
-			border-top-color: var(--white);
-		}
 	}
-	.tippy-popper[x-placement^="bottom"] .tippy-tooltip {
-		&.songActions-theme,
-		&.addToPlaylist-theme {
-			.tippy-arrow {
+
+	.tippy-popper[data-placement^="bottom"] {
+		&[data-theme~="songActions"],
+		&[data-theme~="addToPlaylist"] {
+			> .tippy-arrow::before {
 				border-bottom-color: var(--dark-grey-2);
 			}
 		}
-		&.dark-theme .tippy-arrow {
-			border-bottom-color: var(--white);
-		}
 	}
-	.tippy-popper[x-placement^="left"] .tippy-tooltip {
-		&.songActions-theme,
-		&.addToPlaylist-theme {
-			.tippy-arrow {
+
+	.tippy-popper[data-placement^="left"] {
+		&[data-theme~="songActions"],
+		&[data-theme~="addToPlaylist"] {
+			> .tippy-arrow::before {
 				border-left-color: var(--dark-grey-2);
 			}
 		}
-		&.dark-theme .tippy-arrow {
-			border-left-color: var(--white);
-		}
 	}
-	.tippy-popper[x-placement^="right"] .tippy-tooltip {
-		&.songActions-theme,
-		&.addToPlaylist-theme {
-			.tippy-arrow {
+
+	.tippy-box[data-placement^="right"] {
+		&[data-theme~="songActions"],
+		&[data-theme~="addToPlaylist"] {
+			> .tippy-arrow::before {
 				border-right-color: var(--dark-grey-2);
 			}
 		}
-		&.dark-theme .tippy-arrow {
-			border-right-color: var(--white);
-		}
 	}
 }
 
-.tippy-tooltip.info-theme {
+.tippy-box[data-theme~="info"] {
 	font-size: 12px;
 	letter-spacing: 1px;
 }
 
-.tippy-tooltip.confirm-theme {
+.tippy-box[data-theme~="confirm"] {
 	background-color: var(--red);
 	padding: 5px 10px;
+
 	a {
 		color: var(--white);
+		border-bottom: 0;
 		font-size: 15px;
 		font-weight: 600;
+
 		&:hover,
 		&:focus {
 			filter: brightness(90%);
@@ -478,7 +483,7 @@ a {
 	}
 }
 
-.tippy-tooltip.songActions-theme {
+.tippy-box[data-theme~="songActions"] {
 	font-size: 15px;
 	padding: 5px 10px;
 	border: 1px solid var(--light-grey-3);
@@ -541,52 +546,55 @@ a {
 	}
 }
 
-.tippy-popper[x-placement^="top"] .tippy-tooltip {
-	&.songActions-theme,
-	&.addToPlaylist-theme {
-		.tippy-arrow {
+.tippy-box[data-placement^="top"] {
+	&[data-theme~="songActions"],
+	&[data-theme~="addToPlaylist"] {
+		> .tippy-arrow::before {
 			border-top-color: var(--white);
 		}
 	}
-	&.confirm-theme .tippy-arrow {
+	&[data-theme~="confirm"] > .tippy-arrow::before {
 		border-top-color: var(--red);
 	}
 }
-.tippy-popper[x-placement^="bottom"] .tippy-tooltip {
-	&.songActions-theme,
-	&.addToPlaylist-theme {
-		.tippy-arrow {
+
+.tippy-box[data-placement^="bottom"] {
+	&[data-theme~="songActions"],
+	&[data-theme~="addToPlaylist"] {
+		> .tippy-arrow::before {
 			border-bottom-color: var(--white);
 		}
 	}
-	&.confirm-theme .tippy-arrow {
+	&[data-theme~="confirm"] > .tippy-arrow::before {
 		border-bottom-color: var(--red);
 	}
 }
-.tippy-popper[x-placement^="left"] .tippy-tooltip {
-	&.songActions-theme,
-	&.addToPlaylist-theme {
-		.tippy-arrow {
+
+.tippy-box[data-placement^="left"] {
+	&[data-theme~="songActions"],
+	&[data-theme~="addToPlaylist"] {
+		> .tippy-arrow::before {
 			border-left-color: var(--white);
 		}
 	}
-	&.confirm-theme .tippy-arrow {
+	&[data-theme~="confirm"] > .tippy-arrow::before {
 		border-left-color: var(--red);
 	}
 }
-.tippy-popper[x-placement^="right"] .tippy-tooltip {
-	&.songActions-theme,
-	&.addToPlaylist-theme {
-		.tippy-arrow {
+
+.tippy-box[data-placement^="right"] {
+	&[data-theme~="songActions"],
+	&[data-theme~="addToPlaylist"] {
+		> .tippy-arrow::before {
 			border-right-color: var(--white);
 		}
 	}
-	&.confirm-theme .tippy-arrow {
+	&[data-theme~="confirm"] > .tippy-arrow::before {
 		border-right-color: var(--red);
 	}
 }
 
-.tippy-tooltip.addToPlaylist-theme {
+.tippy-box[data-theme~="addToPlaylist"] {
 	font-size: 15px;
 	padding: 5px;
 	border: 1px solid var(--light-grey-3);

+ 32 - 32
frontend/src/components/AddToPlaylistDropdown.vue

@@ -1,8 +1,8 @@
 <template>
 	<tippy
 		class="addToPlaylistDropdown"
-		touch="true"
-		interactive="true"
+		:touch="true"
+		:interactive="true"
 		:placement="placement"
 		theme="addToPlaylist"
 		trigger="click"
@@ -18,37 +18,37 @@
 			}
 		"
 	>
-		<template #trigger>
-			<slot name="button" />
-		</template>
+		<slot name="button" ref="trigger" />
 
-		<div class="nav-dropdown-items" v-if="playlists.length > 0">
-			<button
-				class="nav-item"
-				href="#"
-				v-for="(playlist, index) in playlists"
-				:key="playlist._id"
-				@click.prevent="toggleSongInPlaylist(index)"
-				:title="playlist.displayName"
-			>
-				<p class="control is-expanded checkbox-control">
-					<label class="switch">
-						<input
-							type="checkbox"
-							:id="index"
-							:checked="hasSong(playlist)"
-							@click="toggleSongInPlaylist(index)"
-						/>
-						<span class="slider round"></span>
-					</label>
-					<label :for="index">
-						<span></span>
-						<p>{{ playlist.displayName }}</p>
-					</label>
-				</p>
-			</button>
-		</div>
-		<p v-else>You haven't created any playlists.</p>
+		<template #content>
+			<div class="nav-dropdown-items" v-if="playlists.length > 0">
+				<button
+					class="nav-item"
+					href="#"
+					v-for="(playlist, index) in playlists"
+					:key="playlist._id"
+					@click.prevent="toggleSongInPlaylist(index)"
+					:title="playlist.displayName"
+				>
+					<p class="control is-expanded checkbox-control">
+						<label class="switch">
+							<input
+								type="checkbox"
+								:id="index"
+								:checked="hasSong(playlist)"
+								@click="toggleSongInPlaylist(index)"
+							/>
+							<span class="slider round"></span>
+						</label>
+						<label :for="index">
+							<span></span>
+							<p>{{ playlist.displayName }}</p>
+						</label>
+					</p>
+				</button>
+			</div>
+			<p v-else>You haven't created any playlists.</p>
+		</template>
 	</tippy>
 </template>
 

+ 10 - 10
frontend/src/components/Confirm.vue

@@ -1,22 +1,21 @@
 <template>
 	<tippy
-		interactive="true"
-		touch="true"
+		:interactive="true"
+		:touch="true"
 		:placement="placement"
 		theme="confirm"
 		ref="confirm"
 		trigger="click"
-		class="button-with-tooltip"
 		@hide="clickedOnce = false"
 	>
-		<template #trigger>
-			<div @click.shift.stop="confirm(true)" @click.exact="confirm()">
-				<slot />
-			</div>
+		<div @click.shift.stop="confirm(true)" @click.exact="confirm()">
+			<slot name="trigger" ref="trigger" />
+		</div>
+		<template #content>
+			<a @click="confirm(null, $event)">
+				Click to Confirm
+			</a>
 		</template>
-		<a @click="confirm(null, $event)">
-			Click to Confirm
-		</a>
 	</tippy>
 </template>
 
@@ -28,6 +27,7 @@ export default {
 			default: "top"
 		}
 	},
+	emits: ["confirm"],
 	data() {
 		return {
 			clickedOnce: false

+ 38 - 40
frontend/src/components/Queue.vue

@@ -1,25 +1,25 @@
 <template>
 	<div id="queue">
 		<draggable
+			tag="transition-group"
+			:component-data="{
+				name: !drag ? 'draggable-list-transition' : null
+			}"
 			:class="{
 				'actionable-button-hidden': !actionableButtonVisible,
 				'scrollable-list': true
 			}"
 			v-if="queue.length > 0"
 			v-model="queue"
+			item-key="_id"
 			v-bind="dragOptions"
 			@start="drag = true"
 			@end="drag = false"
 			@change="repositionSongInQueue"
 		>
-			<transition-group
-				type="transition"
-				:name="!drag ? 'draggable-list-transition' : null"
-			>
+			<template #item="{element, index}">
 				<song-item
-					v-for="(song, index) in queue"
-					:key="`queue-${song._id}`"
-					:song="song"
+					:song="element"
 					:requested-by="
 						station.type === 'community' &&
 							station.partyMode === true
@@ -29,42 +29,40 @@
 					}"
 					:disabled-actions="[]"
 				>
-					<div
-						v-if="isAdminOnly() || isOwnerOnly()"
-						class="song-actions"
-						slot="actions"
-					>
-						<confirm
-							v-if="isOwnerOnly() || isAdminOnly()"
-							placement="left"
-							@confirm="removeFromQueue(song.youtubeId)"
-						>
+					<template v-if="isAdminOnly() || isOwnerOnly()" #actions>
+						<div class="song-actions">
+							<confirm
+								v-if="isOwnerOnly() || isAdminOnly()"
+								placement="left"
+								@confirm="removeFromQueue(element.youtubeId)"
+							>
+								<i
+									class="material-icons delete-icon"
+									content="Remove Song from Queue"
+									v-tippy
+									>delete_forever</i
+								>
+							</confirm>
+							<i
+								class="material-icons"
+								v-if="index > 0"
+								@click="moveSongToTop(element, index)"
+								content="Move to top of Queue"
+								v-tippy
+								>vertical_align_top</i
+							>
 							<i
-								class="material-icons delete-icon"
-								content="Remove Song from Queue"
+								v-if="queue.length - 1 !== index"
+								@click="moveSongToBottom(element, index)"
+								class="material-icons"
+								content="Move to bottom of Queue"
 								v-tippy
-								>delete_forever</i
+								>vertical_align_bottom</i
 							>
-						</confirm>
-						<i
-							class="material-icons"
-							v-if="index > 0"
-							@click="moveSongToTop(song, index)"
-							content="Move to top of Queue"
-							v-tippy
-							>vertical_align_top</i
-						>
-						<i
-							v-if="queue.length - 1 !== index"
-							@click="moveSongToBottom(song, index)"
-							class="material-icons"
-							content="Move to bottom of Queue"
-							v-tippy
-							>vertical_align_bottom</i
-						>
-					</div>
+						</div>
+					</template>
 				</song-item>
-			</transition-group>
+			</template>
 		</draggable>
 		<p class="nothing-here-text" v-else>
 			There are no songs currently queued
@@ -199,7 +197,7 @@ export default {
 			socket: "websockets/getSocket"
 		})
 	},
-	updated() {
+	onUpdated() {
 		// check if actionable button is visible, if not: set max-height of queue items to 100%
 		if (
 			document

+ 1 - 0
frontend/src/components/SaveButton.vue

@@ -17,6 +17,7 @@ export default {
 	props: {
 		type: { type: String, default: "save" } // enum: ["save", "save-and-close"]
 	},
+	emits: ["clicked"],
 	data() {
 		return {
 			status: "default" // enum: ["default", "disabled", "save-failure", "save-success"],

+ 54 - 50
frontend/src/components/SongItem.vue

@@ -72,67 +72,71 @@
 			>
 				<tippy
 					v-if="loggedIn"
-					touch="true"
-					interactive="true"
+					:touch="true"
+					:interactive="true"
 					placement="left"
 					theme="songActions"
 					ref="songActions"
 					trigger="click"
 				>
-					<template #trigger>
-						<i
-							class="material-icons action-dropdown-icon"
-							content="Song Options"
-							v-tippy
-							>more_horiz</i
-						>
-					</template>
-					<a
-						v-if="disabledActions.indexOf('youtube') === -1"
-						target="_blank"
-						:href="
-							`https://www.youtube.com/watch?v=${song.youtubeId}`
-						"
-						content="View on Youtube"
-						v-tippy
-					>
-						<div class="youtube-icon"></div>
-					</a>
 					<i
-						v-if="disabledActions.indexOf('report') === -1"
-						class="material-icons report-icon"
-						@click="report(song)"
-						content="Report Song"
+						class="material-icons action-dropdown-icon"
+						content="Song Options"
 						v-tippy
+						>more_horiz</i
 					>
-						flag
-					</i>
-					<add-to-playlist-dropdown
-						v-if="disabledActions.indexOf('addToPlaylist') === -1"
-						:song="song"
-					>
+
+					<template #content>
+						<a
+							v-if="disabledActions.indexOf('youtube') === -1"
+							target="_blank"
+							:href="
+								`https://www.youtube.com/watch?v=${song.youtubeId}`
+							"
+							content="View on Youtube"
+							v-tippy
+						>
+							<div class="youtube-icon"></div>
+						</a>
 						<i
-							slot="button"
-							class="material-icons add-to-playlist-icon"
-							content="Add Song to Playlist"
+							v-if="disabledActions.indexOf('report') === -1"
+							class="material-icons report-icon"
+							@click="report(song)"
+							content="Report Song"
 							v-tippy
-							>playlist_add</i
 						>
-					</add-to-playlist-dropdown>
-					<i
-						v-if="
-							loggedIn &&
-								userRole === 'admin' &&
-								disabledActions.indexOf('edit') === -1
-						"
-						class="material-icons edit-icon"
-						@click="edit(song)"
-						content="Edit Song"
-						v-tippy
-					>
-						edit
-					</i>
-					<slot name="actions" />
+							flag
+						</i>
+						<add-to-playlist-dropdown
+							v-if="
+								disabledActions.indexOf('addToPlaylist') === -1
+							"
+							:song="song"
+						>
+							<template #button>
+								<i
+									class="material-icons add-to-playlist-icon"
+									content="Add Song to Playlist"
+									v-tippy
+									>playlist_add</i
+								>
+							</template>
+						</add-to-playlist-dropdown>
+						<i
+							v-if="
+								loggedIn &&
+									userRole === 'admin' &&
+									disabledActions.indexOf('edit') === -1
+							"
+							class="material-icons edit-icon"
+							@click="edit(song)"
+							content="Edit Song"
+							v-tippy
+						>
+							edit
+						</i>
+						<slot name="actions" />
+					</template>
 				</tippy>
 				<a
 					v-if="

+ 1 - 4
frontend/src/components/layout/MainFooter.vue

@@ -9,10 +9,7 @@
 					><img src="/assets/blue_wordmark.png" alt="Musare"
 				/></a>
 				<div id="footer-links">
-					<a
-						:href="`${this.github}`"
-						target="_blank"
-						title="GitHub Repository"
+					<a :href="github" target="_blank" title="GitHub Repository"
 						>GitHub</a
 					>
 					<router-link title="About Musare" to="/about"

+ 2 - 2
frontend/src/components/layout/MainHeader.vue

@@ -3,8 +3,8 @@
 		<div class="nav-left">
 			<router-link v-if="!hideLogo" class="nav-item is-brand" to="/">
 				<img
-					:src="`${this.siteSettings.logo_white}`"
-					:alt="`${this.siteSettings.sitename}` || `Musare`"
+					:src="siteSettings.logo_white"
+					:alt="siteSettings.sitename || `Musare`"
 				/>
 			</router-link>
 		</div>

+ 4 - 4
frontend/src/components/modals/EditNews.vue

@@ -3,7 +3,7 @@
 		class="edit-news-modal"
 		:title="newsId ? 'Edit News' : 'Create News'"
 	>
-		<div slot="body">
+		<template #body>
 			<div id="markdown-editor-and-preview">
 				<div class="column">
 					<p><strong>Markdown</strong></p>
@@ -18,8 +18,8 @@
 					></div>
 				</div>
 			</div>
-		</div>
-		<div slot="footer">
+		</template>
+		<template #footer>
 			<p class="control select">
 				<select v-model="status">
 					<option value="draft">Draft</option>
@@ -53,7 +53,7 @@
 					}}
 				</span>
 			</div>
-		</div>
+		</template>
 	</modal>
 </template>
 

+ 2 - 2
frontend/src/components/modals/EditPlaylist/Tabs/Youtube.vue

@@ -71,7 +71,7 @@
 				:key="result.id"
 				:result="result"
 			>
-				<div slot="actions">
+				<template #actions>
 					<transition name="search-query-actions" mode="out-in">
 						<a
 							class="button is-success"
@@ -93,7 +93,7 @@
 							Add to playlist
 						</a>
 					</transition>
-				</div>
+				</template>
 			</search-query-item>
 
 			<a

+ 171 - 168
frontend/src/components/modals/EditPlaylist/index.vue

@@ -5,187 +5,189 @@
 		"
 		class="edit-playlist-modal"
 	>
-		<div
-			slot="body"
-			:class="{
-				'view-only': !isEditable(),
-				'edit-playlist-modal-inner-container': true
-			}"
-		>
-			<div id="first-column">
-				<div id="playlist-info-section" class="section">
-					<h3>{{ playlist.displayName }}</h3>
-					<h5>Song Count: {{ playlist.songs.length }}</h5>
-					<h5>Duration: {{ totalLength() }}</h5>
-				</div>
+		<template #body>
+			<div
+				:class="{
+					'view-only': !isEditable(),
+					'edit-playlist-modal-inner-container': true
+				}"
+			>
+				<div id="first-column">
+					<div id="playlist-info-section" class="section">
+						<h3>{{ playlist.displayName }}</h3>
+						<h5>Song Count: {{ playlist.songs.length }}</h5>
+						<h5>Duration: {{ totalLength() }}</h5>
+					</div>
 
-				<div id="tabs-container">
-					<div id="tab-selection">
-						<button
-							class="button is-default"
-							:class="{ selected: tab === 'settings' }"
-							ref="settings-tab"
-							@click="showTab('settings')"
+					<div id="tabs-container">
+						<div id="tab-selection">
+							<button
+								class="button is-default"
+								:class="{ selected: tab === 'settings' }"
+								ref="settings-tab"
+								@click="showTab('settings')"
+								v-if="
+									userId === playlist.createdBy ||
+										isEditable() ||
+										(playlist.type === 'genre' && isAdmin())
+								"
+							>
+								Settings
+							</button>
+							<button
+								class="button is-default"
+								:class="{ selected: tab === 'youtube' }"
+								ref="youtube-tab"
+								@click="showTab('youtube')"
+								v-if="isEditable()"
+							>
+								YouTube
+							</button>
+						</div>
+						<settings
+							class="tab"
+							v-show="tab === 'settings'"
 							v-if="
 								userId === playlist.createdBy ||
 									isEditable() ||
 									(playlist.type === 'genre' && isAdmin())
 							"
-						>
-							Settings
-						</button>
-						<button
-							class="button is-default"
-							:class="{ selected: tab === 'youtube' }"
-							ref="youtube-tab"
-							@click="showTab('youtube')"
+						/>
+						<youtube
+							class="tab"
+							v-show="tab === 'youtube'"
 							v-if="isEditable()"
-						>
-							YouTube
-						</button>
+						/>
 					</div>
-					<settings
-						class="tab"
-						v-show="tab === 'settings'"
-						v-if="
-							userId === playlist.createdBy ||
-								isEditable() ||
-								(playlist.type === 'genre' && isAdmin())
-						"
-					/>
-					<youtube
-						class="tab"
-						v-show="tab === 'youtube'"
-						v-if="isEditable()"
-					/>
-				</div>
 
-				<!--
+					<!--
 
 				<div
 					id="import-from-youtube-section"
 					 -->
-			</div>
-
-			<div id="second-column">
-				<div id="rearrange-songs-section" class="section">
-					<div v-if="isEditable()">
-						<h4 class="section-title">Rearrange Songs</h4>
-
-						<p class="section-description">
-							Drag and drop songs to change their order
-						</p>
-
-						<hr class="section-horizontal-rule" />
-					</div>
+				</div>
 
-					<aside class="menu">
-						<draggable
-							class="menu-list scrollable-list"
-							tag="ul"
-							v-if="playlistSongs.length > 0"
-							v-model="playlistSongs"
-							v-bind="dragOptions"
-							@start="drag = true"
-							@end="drag = false"
-							@change="repositionSong"
-						>
-							<transition-group
-								type="transition"
-								:name="
-									!drag ? 'draggable-list-transition' : null
-								"
+				<div id="second-column">
+					<div id="rearrange-songs-section" class="section">
+						<div v-if="isEditable()">
+							<h4 class="section-title">Rearrange Songs</h4>
+
+							<p class="section-description">
+								Drag and drop songs to change their order
+							</p>
+
+							<hr class="section-horizontal-rule" />
+						</div>
+
+						<aside class="menu">
+							<draggable
+								tag="transition-group"
+								:component-data="{
+									name: !drag
+										? 'draggable-list-transition'
+										: null
+								}"
+								v-if="playlistSongs.length > 0"
+								v-model="playlistSongs"
+								item-key="_id"
+								v-bind="dragOptions"
+								@start="drag = true"
+								@end="drag = false"
+								@change="repositionSong"
 							>
-								<li
-									v-for="(song, index) in playlistSongs"
-									:key="`key-${song._id}`"
-								>
-									<song-item
-										:song="song"
-										:class="{
-											'item-draggable': isEditable()
-										}"
-									>
-										<div
-											class="song-actions"
-											slot="actions"
+								<template #item="{element, index}">
+									<div class="menu-list scrollable-list">
+										<song-item
+											:song="element"
+											:class="{
+												'item-draggable': isEditable()
+											}"
 										>
-											<i
-												class="material-icons add-to-queue-icon"
-												v-if="
-													station.partyMode &&
-														!station.locked
-												"
-												@click="
-													addSongToQueue(
-														song.youtubeId
-													)
-												"
-												content="Add Song to Queue"
-												v-tippy
-												>queue</i
-											>
-											<confirm
-												v-if="
-													userId ===
-														playlist.createdBy ||
-														isEditable()
-												"
-												placement="left"
-												@confirm="
-													removeSongFromPlaylist(
-														song.youtubeId
-													)
-												"
-											>
-												<i
-													class="material-icons delete-icon"
-													content="Remove Song from Playlist"
-													v-tippy
-													>delete_forever</i
-												>
-											</confirm>
-											<i
-												class="material-icons"
-												v-if="isEditable() && index > 0"
-												@click="
-													moveSongToTop(song, index)
-												"
-												content="Move to top of Playlist"
-												v-tippy
-												>vertical_align_top</i
-											>
-											<i
-												v-if="
-													isEditable() &&
-														playlistSongs.length -
-															1 !==
-															index
-												"
-												@click="
-													moveSongToBottom(
-														song,
-														index
-													)
-												"
-												class="material-icons"
-												content="Move to bottom of Playlist"
-												v-tippy
-												>vertical_align_bottom</i
-											>
-										</div>
-									</song-item>
-								</li>
-							</transition-group>
-						</draggable>
-						<p v-else class="nothing-here-text">
-							This playlist doesn't have any songs.
-						</p>
-					</aside>
+											<template #actions>
+												<div class="song-actions">
+													<i
+														class="material-icons add-to-queue-icon"
+														v-if="
+															station.partyMode &&
+																!station.locked
+														"
+														@click="
+															addSongToQueue(
+																element.youtubeId
+															)
+														"
+														content="Add Song to Queue"
+														v-tippy
+														>queue</i
+													>
+													<confirm
+														v-if="
+															userId ===
+																playlist.createdBy ||
+																isEditable()
+														"
+														placement="left"
+														@confirm="
+															removeSongFromPlaylist(
+																element.youtubeId
+															)
+														"
+													>
+														<i
+															class="material-icons delete-icon"
+															content="Remove Song from Playlist"
+															v-tippy
+															>delete_forever</i
+														>
+													</confirm>
+													<i
+														class="material-icons"
+														v-if="
+															isEditable() &&
+																index > 0
+														"
+														@click="
+															moveSongToTop(
+																element,
+																index
+															)
+														"
+														content="Move to top of Playlist"
+														v-tippy
+														>vertical_align_top</i
+													>
+													<i
+														v-if="
+															isEditable() &&
+																playlistSongs.length -
+																	1 !==
+																	index
+														"
+														@click="
+															moveSongToBottom(
+																element,
+																index
+															)
+														"
+														class="material-icons"
+														content="Move to bottom of Playlist"
+														v-tippy
+														>vertical_align_bottom</i
+													>
+												</div>
+											</template>
+										</song-item>
+									</div>
+								</template>
+							</draggable>
+							<p v-else class="nothing-here-text">
+								This playlist doesn't have any songs.
+							</p>
+						</aside>
+					</div>
 				</div>
-			</div>
 
-			<!--
+				<!--
 			
 			
 			<button
@@ -197,14 +199,15 @@
 			</button>
 			<h5>Edit playlist details:</h5>
 			 -->
-		</div>
-		<div slot="footer">
+			</div>
+		</template>
+		<template #footer>
 			<a
 				class="button is-default"
 				v-if="
-					this.userId === this.playlist.createdBy ||
+					userId === playlist.createdBy ||
 						isEditable() ||
-						this.playlist.privacy === 'public'
+						playlist.privacy === 'public'
 				"
 				@click="downloadPlaylist()"
 				href="#"
@@ -232,7 +235,7 @@
 					<a class="button is-danger"> Remove Playlist </a>
 				</confirm>
 			</div>
-		</div>
+		</template>
 	</modal>
 </template>
 

+ 5 - 5
frontend/src/components/modals/EditSong/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<div>
 		<modal title="Edit Song" class="song-modal">
-			<div slot="body">
+			<template #body>
 				<div class="left-section">
 					<div class="top-section">
 						<div class="player-section">
@@ -330,8 +330,8 @@
 						<reports class="tab" v-show="tab === 'reports'" />
 					</div>
 				</div>
-			</div>
-			<div slot="footer">
+			</template>
+			<template #footer>
 				<save-button ref="saveButton" @clicked="save(song, false)" />
 				<save-button
 					ref="saveAndCloseButton"
@@ -393,7 +393,7 @@
 						</button>
 					</confirm> -->
 				</div>
-			</div>
+			</template>
 		</modal>
 		<floating-box id="genreHelper" ref="genreHelper">
 			<template #body>
@@ -938,7 +938,7 @@ export default {
 
 		*/
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		this.playerReady = false;
 		clearInterval(this.interval);
 		clearInterval(this.activityWatchVideoDataInterval);

+ 4 - 4
frontend/src/components/modals/EditUser.vue

@@ -1,7 +1,7 @@
 <template>
 	<div>
 		<modal title="Edit User">
-			<div slot="body" v-if="user && user._id">
+			<template #body v-if="user && user._id">
 				<p class="control has-addons">
 					<input
 						v-model="user.username"
@@ -60,8 +60,8 @@
 					/>
 					<a class="button is-error" @click="banUser()">Ban user</a>
 				</p>
-			</div>
-			<div slot="footer">
+			</template>
+			<template #footer>
 				<!--button class='button is-warning'>
 					<span>&nbsp;Send Verification Email</span>
 				</button>
@@ -71,7 +71,7 @@
 				<button class="button is-warning" @click="removeSessions()">
 					<span>&nbsp;Remove all sessions</span>
 				</button>
-			</div>
+			</template>
 		</modal>
 	</div>
 </template>

+ 393 - 360
frontend/src/components/modals/ManageStationKris/Tabs/Playlists.vue

@@ -73,208 +73,9 @@
 						:playlist="playlist"
 						:show-owner="true"
 					>
-						<i
-							class="material-icons"
-							slot="item-icon"
-							v-if="
-								isAllowedToParty() && isSelected(playlist._id)
-							"
-							content="This playlist is currently selected"
-							v-tippy
-						>
-							radio
-						</i>
-						<i
-							class="material-icons"
-							slot="item-icon"
-							v-else-if="
-								isOwnerOrAdmin() &&
-									isPlaylistMode() &&
-									isIncluded(playlist._id)
-							"
-							content="This playlist is currently included"
-							v-tippy
-						>
-							play_arrow
-						</i>
-						<i
-							class="material-icons excluded-icon"
-							slot="item-icon"
-							v-else-if="
-								isOwnerOrAdmin() && isExcluded(playlist._id)
-							"
-							content="This playlist is currently excluded"
-							v-tippy
-						>
-							block
-						</i>
-						<i
-							class="material-icons"
-							slot="item-icon"
-							v-else
-							:content="
-								isPartyMode()
-									? 'This playlist is currently not selected or excluded'
-									: 'This playlist is currently not included or excluded'
-							"
-							v-tippy
-						>
-							play_disabled
-						</i>
-						<div class="icons-group" slot="actions">
-							<i
-								v-if="isExcluded(playlist._id)"
-								class="material-icons stop-icon"
-								content="This playlist is blacklisted in this station"
-								v-tippy="{ theme: 'info' }"
-								>play_disabled</i
-							>
-							<confirm
-								v-if="isPartyMode() && isSelected(playlist._id)"
-								@confirm="deselectPartyPlaylist(playlist._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Stop playing songs from this playlist"
-									v-tippy
-								>
-									stop
-								</i>
-							</confirm>
-							<confirm
-								v-if="
-									isOwnerOrAdmin() &&
-										isPlaylistMode() &&
-										isIncluded(playlist._id)
-								"
-								@confirm="removeIncludedPlaylist(playlist._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Stop playing songs from this playlist"
-									v-tippy
-								>
-									stop
-								</i>
-							</confirm>
-							<i
-								v-if="
-									isPartyMode() &&
-										!isSelected(playlist._id) &&
-										!isExcluded(playlist._id)
-								"
-								@click="selectPartyPlaylist(playlist)"
-								class="material-icons play-icon"
-								content="Request songs from this playlist"
-								v-tippy
-								>play_arrow</i
-							>
-							<i
-								v-if="
-									isOwnerOrAdmin() &&
-										isPlaylistMode() &&
-										!isIncluded(playlist._id) &&
-										!isExcluded(playlist._id)
-								"
-								@click="includePlaylist(playlist)"
-								class="material-icons play-icon"
-								:content="'Play songs from this playlist'"
-								v-tippy
-								>play_arrow</i
-							>
-							<confirm
-								v-if="
-									isOwnerOrAdmin() &&
-										!isExcluded(playlist._id)
-								"
-								@confirm="blacklistPlaylist(playlist._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Blacklist Playlist"
-									v-tippy
-									>block</i
-								>
-							</confirm>
-							<confirm
-								v-if="
-									isOwnerOrAdmin() && isExcluded(playlist._id)
-								"
-								@confirm="removeExcludedPlaylist(playlist._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Stop blacklisting songs from this playlist"
-									v-tippy
-								>
-									stop
-								</i>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === myUserId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-if="
-									playlist.createdBy !== myUserId &&
-										(playlist.privacy === 'public' ||
-											isAdmin())
-								"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
-					</playlist-item>
-					<button
-						v-if="resultsLeftCount > 0"
-						class="button is-primary load-more-button"
-						@click="searchForPlaylists(search.page + 1)"
-					>
-						Load {{ nextPageResultsCount }} more results
-					</button>
-				</div>
-			</div>
-			<div
-				v-if="station.type === 'community'"
-				class="tab"
-				v-show="tab === 'my-playlists'"
-			>
-				<button
-					class="button is-primary"
-					id="create-new-playlist-button"
-					@click="openModal('createPlaylist')"
-				>
-					Create new playlist
-				</button>
-				<draggable
-					class="menu-list scrollable-list"
-					v-if="playlists.length > 0"
-					v-model="playlists"
-					v-bind="dragOptions"
-					@start="drag = true"
-					@end="drag = false"
-					@change="savePlaylistOrder"
-				>
-					<transition-group
-						type="transition"
-						:name="!drag ? 'draggable-list-transition' : null"
-					>
-						<playlist-item
-							class="item-draggable"
-							v-for="playlist in playlists"
-							:key="playlist._id"
-							:playlist="playlist"
-						>
+						<template #item-icon>
 							<i
 								class="material-icons"
-								slot="item-icon"
 								v-if="
 									isAllowedToParty() &&
 										isSelected(playlist._id)
@@ -286,7 +87,6 @@
 							</i>
 							<i
 								class="material-icons"
-								slot="item-icon"
 								v-else-if="
 									isOwnerOrAdmin() &&
 										isPlaylistMode() &&
@@ -299,7 +99,6 @@
 							</i>
 							<i
 								class="material-icons excluded-icon"
-								slot="item-icon"
 								v-else-if="
 									isOwnerOrAdmin() && isExcluded(playlist._id)
 								"
@@ -310,7 +109,6 @@
 							</i>
 							<i
 								class="material-icons"
-								slot="item-icon"
 								v-else
 								:content="
 									isPartyMode()
@@ -321,36 +119,16 @@
 							>
 								play_disabled
 							</i>
-							<div slot="actions">
-								<!-- <i
+						</template>
+
+						<template #actions>
+							<div class="icons-group">
+								<i
 									v-if="isExcluded(playlist._id)"
 									class="material-icons stop-icon"
 									content="This playlist is blacklisted in this station"
 									v-tippy="{ theme: 'info' }"
 									>play_disabled</i
-								> -->
-								<i
-									v-if="
-										isPartyMode() &&
-											!isSelected(playlist._id)
-									"
-									@click="selectPartyPlaylist(playlist)"
-									class="material-icons play-icon"
-									content="Request songs from this playlist"
-									v-tippy
-									>play_arrow</i
-								>
-								<i
-									v-if="
-										isPlaylistMode() &&
-											isOwnerOrAdmin() &&
-											!isSelected(playlist._id)
-									"
-									@click="includePlaylist(playlist)"
-									class="material-icons play-icon"
-									content="Play songs from this playlist"
-									v-tippy
-									>play_arrow</i
 								>
 								<confirm
 									v-if="
@@ -363,15 +141,16 @@
 								>
 									<i
 										class="material-icons stop-icon"
-										content="Stop requesting songs from this playlist"
+										content="Stop playing songs from this playlist"
 										v-tippy
-										>stop</i
 									>
+										stop
+									</i>
 								</confirm>
 								<confirm
 									v-if="
-										isPlaylistMode() &&
-											isOwnerOrAdmin() &&
+										isOwnerOrAdmin() &&
+											isPlaylistMode() &&
 											isIncluded(playlist._id)
 									"
 									@confirm="
@@ -382,9 +161,35 @@
 										class="material-icons stop-icon"
 										content="Stop playing songs from this playlist"
 										v-tippy
-										>stop</i
 									>
+										stop
+									</i>
 								</confirm>
+								<i
+									v-if="
+										isPartyMode() &&
+											!isSelected(playlist._id) &&
+											!isExcluded(playlist._id)
+									"
+									@click="selectPartyPlaylist(playlist)"
+									class="material-icons play-icon"
+									content="Request songs from this playlist"
+									v-tippy
+									>play_arrow</i
+								>
+								<i
+									v-if="
+										isOwnerOrAdmin() &&
+											isPlaylistMode() &&
+											!isIncluded(playlist._id) &&
+											!isExcluded(playlist._id)
+									"
+									@click="includePlaylist(playlist)"
+									class="material-icons play-icon"
+									:content="'Play songs from this playlist'"
+									v-tippy
+									>play_arrow</i
+								>
 								<confirm
 									v-if="
 										isOwnerOrAdmin() &&
@@ -417,15 +222,225 @@
 									</i>
 								</confirm>
 								<i
+									v-if="playlist.createdBy === myUserId"
 									@click="showPlaylist(playlist._id)"
 									class="material-icons edit-icon"
 									content="Edit Playlist"
 									v-tippy
 									>edit</i
 								>
+								<i
+									v-if="
+										playlist.createdBy !== myUserId &&
+											(playlist.privacy === 'public' ||
+												isAdmin())
+									"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="View Playlist"
+									v-tippy
+									>visibility</i
+								>
 							</div>
-						</playlist-item>
-					</transition-group>
+						</template>
+					</playlist-item>
+					<button
+						v-if="resultsLeftCount > 0"
+						class="button is-primary load-more-button"
+						@click="searchForPlaylists(search.page + 1)"
+					>
+						Load {{ nextPageResultsCount }} more results
+					</button>
+				</div>
+			</div>
+			<div
+				v-if="station.type === 'community'"
+				class="tab"
+				v-show="tab === 'my-playlists'"
+			>
+				<button
+					class="button is-primary"
+					id="create-new-playlist-button"
+					@click="openModal('createPlaylist')"
+				>
+					Create new playlist
+				</button>
+				<draggable
+					tag="transition-group"
+					:component-data="{
+						name: !drag ? 'draggable-list-transition' : null
+					}"
+					v-if="playlists.length > 0"
+					item-key="_id"
+					v-model="playlists"
+					v-bind="dragOptions"
+					@start="drag = true"
+					@end="drag = false"
+					@change="savePlaylistOrder"
+				>
+					<template #item="{element}">
+						<div class="menu-list scrollable-list">
+							<playlist-item
+								class="item-draggable"
+								:playlist="element"
+							>
+								<template #item-icon>
+									<i
+										class="material-icons"
+										v-if="
+											isAllowedToParty() &&
+												isSelected(element._id)
+										"
+										content="This playlist is currently selected"
+										v-tippy
+									>
+										radio
+									</i>
+									<i
+										class="material-icons"
+										v-else-if="
+											isOwnerOrAdmin() &&
+												isPlaylistMode() &&
+												isIncluded(element._id)
+										"
+										content="This playlist is currently included"
+										v-tippy
+									>
+										play_arrow
+									</i>
+									<i
+										class="material-icons excluded-icon"
+										v-else-if="
+											isOwnerOrAdmin() &&
+												isExcluded(element._id)
+										"
+										content="This playlist is currently excluded"
+										v-tippy
+									>
+										block
+									</i>
+									<i
+										class="material-icons"
+										v-else
+										:content="
+											isPartyMode()
+												? 'This playlist is currently not selected or excluded'
+												: 'This playlist is currently not included or excluded'
+										"
+										v-tippy
+									>
+										play_disabled
+									</i>
+								</template>
+
+								<template #actions>
+									<!-- <i
+									v-if="isExcluded(playlist._id)"
+									class="material-icons stop-icon"
+									content="This playlist is blacklisted in this station"
+									v-tippy="{ theme: 'info' }"
+									>play_disabled</i
+								> -->
+									<i
+										v-if="
+											isPartyMode() &&
+												!isSelected(element._id)
+										"
+										@click="selectPartyPlaylist(element)"
+										class="material-icons play-icon"
+										content="Request songs from this playlist"
+										v-tippy
+										>play_arrow</i
+									>
+									<i
+										v-if="
+											isPlaylistMode() &&
+												isOwnerOrAdmin() &&
+												!isSelected(element._id)
+										"
+										@click="includePlaylist(element)"
+										class="material-icons play-icon"
+										content="Play songs from this playlist"
+										v-tippy
+										>play_arrow</i
+									>
+									<confirm
+										v-if="
+											isPartyMode() &&
+												isSelected(element._id)
+										"
+										@confirm="
+											deselectPartyPlaylist(element._id)
+										"
+									>
+										<i
+											class="material-icons stop-icon"
+											content="Stop requesting songs from this playlist"
+											v-tippy
+											>stop</i
+										>
+									</confirm>
+									<confirm
+										v-if="
+											isPlaylistMode() &&
+												isOwnerOrAdmin() &&
+												isIncluded(element._id)
+										"
+										@confirm="
+											removeIncludedPlaylist(element._id)
+										"
+									>
+										<i
+											class="material-icons stop-icon"
+											content="Stop playing songs from this playlist"
+											v-tippy
+											>stop</i
+										>
+									</confirm>
+									<confirm
+										v-if="
+											isOwnerOrAdmin() &&
+												!isExcluded(element._id)
+										"
+										@confirm="
+											blacklistPlaylist(element._id)
+										"
+									>
+										<i
+											class="material-icons stop-icon"
+											content="Blacklist Playlist"
+											v-tippy
+											>block</i
+										>
+									</confirm>
+									<confirm
+										v-if="
+											isOwnerOrAdmin() &&
+												isExcluded(element._id)
+										"
+										@confirm="
+											removeExcludedPlaylist(element._id)
+										"
+									>
+										<i
+											class="material-icons stop-icon"
+											content="Stop blacklisting songs from this playlist"
+											v-tippy
+										>
+											stop
+										</i>
+									</confirm>
+									<i
+										@click="showPlaylist(element._id)"
+										class="material-icons edit-icon"
+										content="Edit Playlist"
+										v-tippy
+										>edit</i
+									>
+								</template>
+							</playlist-item>
+						</div>
+					</template>
 				</draggable>
 				<p v-else class="has-text-centered scrollable-list">
 					You don't have any playlists!
@@ -439,59 +454,65 @@
 						:playlist="playlist"
 						:show-owner="true"
 					>
-						<i
-							class="material-icons"
-							slot="item-icon"
-							content="This playlist is currently selected"
-							v-tippy
-						>
-							radio
-						</i>
-						<div class="icons-group" slot="actions">
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="deselectPartyPlaylist(playlist._id)"
+						<template #item-icon>
+							<i
+								class="material-icons"
+								content="This playlist is currently selected"
+								v-tippy
 							>
+								radio
+							</i>
+						</template>
+
+						<template #actions>
+							<div class="icons-group">
+								<confirm
+									v-if="isOwnerOrAdmin()"
+									@confirm="
+										deselectPartyPlaylist(playlist._id)
+									"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop playing songs from this playlist"
+										v-tippy
+									>
+										stop
+									</i>
+								</confirm>
+								<confirm
+									v-if="isOwnerOrAdmin()"
+									@confirm="blacklistPlaylist(playlist._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Blacklist Playlist"
+										v-tippy
+										>block</i
+									>
+								</confirm>
 								<i
-									class="material-icons stop-icon"
-									content="Stop playing songs from this playlist"
+									v-if="playlist.createdBy === myUserId"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="Edit Playlist"
 									v-tippy
+									>edit</i
 								>
-									stop
-								</i>
-							</confirm>
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="blacklistPlaylist(playlist._id)"
-							>
 								<i
-									class="material-icons stop-icon"
-									content="Blacklist Playlist"
+									v-if="
+										playlist.createdBy !== myUserId &&
+											(playlist.privacy === 'public' ||
+												isAdmin())
+									"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="View Playlist"
 									v-tippy
-									>block</i
+									>visibility</i
 								>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === myUserId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-if="
-									playlist.createdBy !== myUserId &&
-										(playlist.privacy === 'public' ||
-											isAdmin())
-								"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
+							</div>
+						</template>
 					</playlist-item>
 				</div>
 				<p v-else class="has-text-centered scrollable-list">
@@ -510,59 +531,65 @@
 						:playlist="playlist"
 						:show-owner="true"
 					>
-						<i
-							class="material-icons"
-							slot="item-icon"
-							content="This playlist is currently included"
-							v-tippy
-						>
-							play_arrow
-						</i>
-						<div class="icons-group" slot="actions">
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="removeIncludedPlaylist(playlist._id)"
+						<template #item-icon>
+							<i
+								class="material-icons"
+								content="This playlist is currently included"
+								v-tippy
 							>
+								play_arrow
+							</i>
+						</template>
+
+						<template #actions>
+							<div class="icons-group">
+								<confirm
+									v-if="isOwnerOrAdmin()"
+									@confirm="
+										removeIncludedPlaylist(playlist._id)
+									"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop playing songs from this playlist"
+										v-tippy
+									>
+										stop
+									</i>
+								</confirm>
+								<confirm
+									v-if="isOwnerOrAdmin()"
+									@confirm="blacklistPlaylist(playlist._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Blacklist Playlist"
+										v-tippy
+										>block</i
+									>
+								</confirm>
 								<i
-									class="material-icons stop-icon"
-									content="Stop playing songs from this playlist"
+									v-if="playlist.createdBy === myUserId"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="Edit Playlist"
 									v-tippy
+									>edit</i
 								>
-									stop
-								</i>
-							</confirm>
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="blacklistPlaylist(playlist._id)"
-							>
 								<i
-									class="material-icons stop-icon"
-									content="Blacklist Playlist"
+									v-if="
+										playlist.createdBy !== myUserId &&
+											(playlist.privacy === 'public' ||
+												isAdmin())
+									"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="View Playlist"
 									v-tippy
-									>block</i
+									>visibility</i
 								>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === myUserId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-if="
-									playlist.createdBy !== myUserId &&
-										(playlist.privacy === 'public' ||
-											isAdmin())
-								"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
+							</div>
+						</template>
 					</playlist-item>
 				</div>
 				<p v-else class="has-text-centered scrollable-list">
@@ -580,43 +607,49 @@
 						v-for="playlist in excludedPlaylists"
 						:key="`key-${playlist._id}`"
 					>
-						<i
-							class="material-icons excluded-icon"
-							slot="item-icon"
-							content="This playlist is currently excluded"
-							v-tippy
-						>
-							block
-						</i>
-						<div class="icons-group" slot="actions">
-							<confirm
-								@confirm="removeExcludedPlaylist(playlist._id)"
+						<template #item-icon>
+							<i
+								class="material-icons excluded-icon"
+								content="This playlist is currently excluded"
+								v-tippy
 							>
-								<i
-									class="material-icons stop-icon"
-									content="Stop blacklisting songs from this playlist
+								block
+							</i>
+						</template>
+
+						<template #actions>
+							<div class="icons-group">
+								<confirm
+									@confirm="
+										removeExcludedPlaylist(playlist._id)
+									"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop blacklisting songs from this playlist
 							"
+										v-tippy
+										>stop</i
+									>
+								</confirm>
+								<i
+									v-if="playlist.createdBy === userId"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="Edit Playlist"
 									v-tippy
-									>stop</i
+									>edit</i
 								>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === userId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-else
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
+								<i
+									v-else
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="View Playlist"
+									v-tippy
+									>visibility</i
+								>
+							</div>
+						</template>
 					</playlist-item>
 				</div>
 				<p v-else class="has-text-centered scrollable-list">

+ 182 - 172
frontend/src/components/modals/ManageStationKris/Tabs/Settings.vue

@@ -42,119 +42,121 @@
 		<div class="settings-buttons">
 			<div class="small-section">
 				<label class="label">Theme</label>
-				<tippy
-					class="button-wrapper"
-					theme="addToPlaylist"
-					interactive="true"
-					touch="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button :class="station.theme">
 							<i class="material-icons">palette</i>
 							{{ station.theme }}
 						</button>
-					</template>
-					<button
-						class="blue"
-						v-if="station.theme !== 'blue'"
-						@click="updateTheme('blue')"
-					>
-						<i class="material-icons">palette</i>
-						Blue
-					</button>
-					<button
-						class="purple"
-						v-if="station.theme !== 'purple'"
-						@click="updateTheme('purple')"
-					>
-						<i class="material-icons">palette</i>
-						Purple
-					</button>
-					<button
-						class="teal"
-						v-if="station.theme !== 'teal'"
-						@click="updateTheme('teal')"
-					>
-						<i class="material-icons">palette</i>
-						Teal
-					</button>
-					<button
-						class="orange"
-						v-if="station.theme !== 'orange'"
-						@click="updateTheme('orange')"
-					>
-						<i class="material-icons">palette</i>
-						Orange
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="blue"
+								v-if="station.theme !== 'blue'"
+								@click="updateTheme('blue')"
+							>
+								<i class="material-icons">palette</i>
+								Blue
+							</button>
+							<button
+								class="purple"
+								v-if="station.theme !== 'purple'"
+								@click="updateTheme('purple')"
+							>
+								<i class="material-icons">palette</i>
+								Purple
+							</button>
+							<button
+								class="teal"
+								v-if="station.theme !== 'teal'"
+								@click="updateTheme('teal')"
+							>
+								<i class="material-icons">palette</i>
+								Teal
+							</button>
+							<button
+								class="orange"
+								v-if="station.theme !== 'orange'"
+								@click="updateTheme('orange')"
+							>
+								<i class="material-icons">palette</i>
+								Orange
+							</button>
+						</template>
+					</tippy>
+				</div>
 			</div>
 			<div class="small-section">
 				<label class="label">Privacy</label>
-				<tippy
-					class="button-wrapper"
-					theme="addToPlaylist"
-					interactive="true"
-					touch="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button :class="privacyButtons[station.privacy].style">
 							<i class="material-icons">{{
 								privacyButtons[station.privacy].iconName
 							}}</i>
 							{{ station.privacy }}
 						</button>
-					</template>
-					<button
-						class="green"
-						v-if="station.privacy !== 'public'"
-						@click="updatePrivacy('public')"
-					>
-						<i class="material-icons">{{
-							privacyButtons["public"].iconName
-						}}</i>
-						Public
-					</button>
-					<button
-						class="orange"
-						v-if="station.privacy !== 'unlisted'"
-						@click="updatePrivacy('unlisted')"
-					>
-						<i class="material-icons">{{
-							privacyButtons["unlisted"].iconName
-						}}</i>
-						Unlisted
-					</button>
-					<button
-						class="red"
-						v-if="station.privacy !== 'private'"
-						@click="updatePrivacy('private')"
-					>
-						<i class="material-icons">{{
-							privacyButtons["private"].iconName
-						}}</i>
-						Private
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="green"
+								v-if="station.privacy !== 'public'"
+								@click="updatePrivacy('public')"
+							>
+								<i class="material-icons">{{
+									privacyButtons["public"].iconName
+								}}</i>
+								Public
+							</button>
+							<button
+								class="orange"
+								v-if="station.privacy !== 'unlisted'"
+								@click="updatePrivacy('unlisted')"
+							>
+								<i class="material-icons">{{
+									privacyButtons["unlisted"].iconName
+								}}</i>
+								Unlisted
+							</button>
+							<button
+								class="red"
+								v-if="station.privacy !== 'private'"
+								@click="updatePrivacy('private')"
+							>
+								<i class="material-icons">{{
+									privacyButtons["private"].iconName
+								}}</i>
+								Private
+							</button>
+						</template>
+					</tippy>
+				</div>
 			</div>
 			<div class="small-section">
 				<label class="label">Station Mode</label>
-				<tippy
-					v-if="station.type === 'community'"
-					class="button-wrapper"
-					theme="addToPlaylist"
-					touch="true"
-					interactive="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper" v-if="station.type === 'community'">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button
 							:class="{
 								blue: !station.partyMode,
@@ -168,24 +170,27 @@
 							}}</i>
 							{{ station.partyMode ? "Party" : "Playlist" }}
 						</button>
-					</template>
-					<button
-						class="blue"
-						v-if="station.partyMode"
-						@click="updatePartyMode(false)"
-					>
-						<i class="material-icons">playlist_play</i>
-						Playlist
-					</button>
-					<button
-						class="yellow"
-						v-if="!station.partyMode"
-						@click="updatePartyMode(true)"
-					>
-						<i class="material-icons">emoji_people</i>
-						Party
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="blue"
+								v-if="station.partyMode"
+								@click="updatePartyMode(false)"
+							>
+								<i class="material-icons">playlist_play</i>
+								Playlist
+							</button>
+							<button
+								class="yellow"
+								v-if="!station.partyMode"
+								@click="updatePartyMode(true)"
+							>
+								<i class="material-icons">emoji_people</i>
+								Party
+							</button>
+						</template>
+					</tippy>
+				</div>
 				<div v-else class="button-wrapper">
 					<button
 						class="blue"
@@ -199,17 +204,15 @@
 			</div>
 			<div v-if="!station.partyMode" class="small-section">
 				<label class="label">Play Mode</label>
-				<tippy
-					v-if="station.type === 'community'"
-					class="button-wrapper"
-					theme="addToPlaylist"
-					touch="true"
-					interactive="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper" v-if="station.type === 'community'">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button class="blue">
 							<i class="material-icons">{{
 								station.playMode === "random"
@@ -222,24 +225,29 @@
 									: "Sequential"
 							}}
 						</button>
-					</template>
-					<button
-						class="blue"
-						v-if="station.playMode === 'sequential'"
-						@click="updatePlayMode('random')"
-					>
-						<i class="material-icons">shuffle</i>
-						Random
-					</button>
-					<button
-						class="blue"
-						v-if="station.playMode === 'random'"
-						@click="updatePlayMode('sequential')"
-					>
-						<i class="material-icons">format_list_numbered</i>
-						Sequential
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="blue"
+								v-if="station.playMode === 'sequential'"
+								@click="updatePlayMode('random')"
+							>
+								<i class="material-icons">shuffle</i>
+								Random
+							</button>
+							<button
+								class="blue"
+								v-if="station.playMode === 'random'"
+								@click="updatePlayMode('sequential')"
+							>
+								<i class="material-icons"
+									>format_list_numbered</i
+								>
+								Sequential
+							</button>
+						</template>
+					</tippy>
+				</div>
 				<div v-else class="button-wrapper">
 					<button
 						class="blue"
@@ -258,16 +266,15 @@
 				class="small-section"
 			>
 				<label class="label">Queue lock</label>
-				<tippy
-					class="button-wrapper"
-					theme="addToPlaylist"
-					interactive="true"
-					touch="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button
 							:class="{
 								green: station.locked,
@@ -279,24 +286,27 @@
 							}}</i>
 							{{ station.locked ? "Locked" : "Unlocked" }}
 						</button>
-					</template>
-					<button
-						class="green"
-						v-if="!station.locked"
-						@click="updateQueueLock(true)"
-					>
-						<i class="material-icons">lock</i>
-						Locked
-					</button>
-					<button
-						class="red"
-						v-if="station.locked"
-						@click="updateQueueLock(false)"
-					>
-						<i class="material-icons">lock_open</i>
-						Unlocked
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="green"
+								v-if="!station.locked"
+								@click="updateQueueLock(true)"
+							>
+								<i class="material-icons">lock</i>
+								Locked
+							</button>
+							<button
+								class="red"
+								v-if="station.locked"
+								@click="updateQueueLock(false)"
+							>
+								<i class="material-icons">lock_open</i>
+								Unlocked
+							</button>
+						</template>
+					</tippy>
+				</div>
 			</div>
 		</div>
 	</div>

+ 16 - 12
frontend/src/components/modals/ManageStationKris/Tabs/Songs.vue

@@ -64,16 +64,20 @@
 							:key="song._id"
 							:song="song"
 						>
-							<div class="song-actions" slot="actions">
-								<i
-									class="material-icons add-to-queue-icon"
-									v-if="station.partyMode && !station.locked"
-									@click="addSongToQueue(song.youtubeId)"
-									content="Add Song to Queue"
-									v-tippy
-									>queue</i
-								>
-							</div>
+							<template #actions>
+								<div class="song-actions">
+									<i
+										class="material-icons add-to-queue-icon"
+										v-if="
+											station.partyMode && !station.locked
+										"
+										@click="addSongToQueue(song.youtubeId)"
+										content="Add Song to Queue"
+										v-tippy
+										>queue</i
+									>
+								</div>
+							</template>
 						</song-item>
 						<button
 							v-if="resultsLeftCount > 0"
@@ -117,7 +121,7 @@
 							:key="result.id"
 							:result="result"
 						>
-							<div slot="actions">
+							<template #actions>
 								<transition
 									name="search-query-actions"
 									mode="out-in"
@@ -148,7 +152,7 @@
 										Add to queue
 									</a>
 								</transition>
-							</div>
+							</template>
 						</search-query-item>
 
 						<a

+ 1 - 1
frontend/src/components/modals/ManageStationKris/index.vue

@@ -544,7 +544,7 @@ export default {
 			);
 		}
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		this.socket.dispatch(
 			"apis.leaveRoom",
 			`manage-station.${this.stationId}`,

+ 28 - 24
frontend/src/components/modals/ManageStationOwen/Tabs/Blacklist.vue

@@ -28,33 +28,37 @@
 						v-for="playlist in excludedPlaylists"
 						:key="`key-${playlist._id}`"
 					>
-						<div class="icons-group" slot="actions">
-							<confirm @confirm="deselectPlaylist(playlist._id)">
-								<i
-									class="material-icons stop-icon"
-									content="Stop blacklisting songs from this playlist
+						<template #actions>
+							<div class="icons-group">
+								<confirm
+									@confirm="deselectPlaylist(playlist._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop blacklisting songs from this playlist
 							"
+										v-tippy
+										>stop</i
+									>
+								</confirm>
+								<i
+									v-if="playlist.createdBy === userId"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="Edit Playlist"
+									v-tippy
+									>edit</i
+								>
+								<i
+									v-else
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="View Playlist"
 									v-tippy
-									>stop</i
+									>visibility</i
 								>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === userId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-else
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
+							</div>
+						</template>
 					</playlist-item>
 				</div>
 				<p v-else class="has-text-centered scrollable-list">

+ 188 - 181
frontend/src/components/modals/ManageStationOwen/Tabs/Playlists.vue

@@ -36,51 +36,53 @@
 						:playlist="playlist"
 						:show-owner="true"
 					>
-						<div class="icons-group" slot="actions">
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="deselectPlaylist(playlist._id)"
-							>
+						<template #actions>
+							<div class="icons-group">
+								<confirm
+									v-if="isOwnerOrAdmin()"
+									@confirm="deselectPlaylist(playlist._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop playing songs from this playlist"
+										v-tippy
+									>
+										stop
+									</i>
+								</confirm>
+								<confirm
+									v-if="isOwnerOrAdmin()"
+									@confirm="blacklistPlaylist(playlist._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Blacklist Playlist"
+										v-tippy
+										>block</i
+									>
+								</confirm>
 								<i
-									class="material-icons stop-icon"
-									content="Stop playing songs from this playlist"
+									v-if="playlist.createdBy === myUserId"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="Edit Playlist"
 									v-tippy
+									>edit</i
 								>
-									stop
-								</i>
-							</confirm>
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="blacklistPlaylist(playlist._id)"
-							>
 								<i
-									class="material-icons stop-icon"
-									content="Blacklist Playlist"
+									v-if="
+										playlist.createdBy !== myUserId &&
+											(playlist.privacy === 'public' ||
+												isAdmin())
+									"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="View Playlist"
 									v-tippy
-									>block</i
+									>visibility</i
 								>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === myUserId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-if="
-									playlist.createdBy !== myUserId &&
-										(playlist.privacy === 'public' ||
-											isAdmin())
-								"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
+							</div>
+						</template>
 					</playlist-item>
 				</div>
 				<p v-else class="has-text-centered scrollable-list">
@@ -114,84 +116,86 @@
 						:playlist="playlist"
 						:show-owner="true"
 					>
-						<div class="icons-group" slot="actions">
-							<i
-								v-if="isExcluded(playlist._id)"
-								class="material-icons stop-icon"
-								content="This playlist is blacklisted in this station"
-								v-tippy="{ theme: 'info' }"
-								>play_disabled</i
-							>
-							<confirm
-								v-if="
-									(isOwnerOrAdmin() ||
-										(station.type === 'community' &&
-											station.partyMode)) &&
-										isSelected(playlist._id)
-								"
-								@confirm="deselectPlaylist(playlist._id)"
-							>
+						<template #actions>
+							<div class="icons-group">
 								<i
+									v-if="isExcluded(playlist._id)"
 									class="material-icons stop-icon"
-									content="Stop playing songs from this playlist"
+									content="This playlist is blacklisted in this station"
+									v-tippy="{ theme: 'info' }"
+									>play_disabled</i
+								>
+								<confirm
+									v-if="
+										(isOwnerOrAdmin() ||
+											(station.type === 'community' &&
+												station.partyMode)) &&
+											isSelected(playlist._id)
+									"
+									@confirm="deselectPlaylist(playlist._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop playing songs from this playlist"
+										v-tippy
+									>
+										stop
+									</i>
+								</confirm>
+								<i
+									v-if="
+										(isOwnerOrAdmin() ||
+											(station.type === 'community' &&
+												station.partyMode)) &&
+											!isSelected(playlist._id) &&
+											!isExcluded(playlist._id)
+									"
+									@click="selectPlaylist(playlist)"
+									class="material-icons play-icon"
+									:content="
+										station.partyMode
+											? 'Request songs from this playlist'
+											: 'Play songs from this playlist'
+									"
 									v-tippy
+									>play_arrow</i
 								>
-									stop
-								</i>
-							</confirm>
-							<i
-								v-if="
-									(isOwnerOrAdmin() ||
-										(station.type === 'community' &&
-											station.partyMode)) &&
-										!isSelected(playlist._id) &&
-										!isExcluded(playlist._id)
-								"
-								@click="selectPlaylist(playlist)"
-								class="material-icons play-icon"
-								:content="
-									station.partyMode
-										? 'Request songs from this playlist'
-										: 'Play songs from this playlist'
-								"
-								v-tippy
-								>play_arrow</i
-							>
-							<confirm
-								v-if="
-									isOwnerOrAdmin() &&
-										!isExcluded(playlist._id)
-								"
-								@confirm="blacklistPlaylist(playlist._id)"
-							>
+								<confirm
+									v-if="
+										isOwnerOrAdmin() &&
+											!isExcluded(playlist._id)
+									"
+									@confirm="blacklistPlaylist(playlist._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Blacklist Playlist"
+										v-tippy
+										>block</i
+									>
+								</confirm>
 								<i
-									class="material-icons stop-icon"
-									content="Blacklist Playlist"
+									v-if="playlist.createdBy === myUserId"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="Edit Playlist"
 									v-tippy
-									>block</i
+									>edit</i
 								>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === myUserId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-if="
-									playlist.createdBy !== myUserId &&
-										(playlist.privacy === 'public' ||
-											isAdmin())
-								"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
+								<i
+									v-if="
+										playlist.createdBy !== myUserId &&
+											(playlist.privacy === 'public' ||
+												isAdmin())
+									"
+									@click="showPlaylist(playlist._id)"
+									class="material-icons edit-icon"
+									content="View Playlist"
+									v-tippy
+									>visibility</i
+								>
+							</div>
+						</template>
 					</playlist-item>
 					<button
 						v-if="resultsLeftCount > 0"
@@ -215,7 +219,11 @@
 					Create new playlist
 				</button>
 				<draggable
-					class="menu-list scrollable-list"
+					tag="transition-group"
+					:component-data="{
+						name: !drag ? 'draggable-list-transition' : null
+					}"
+					item-key="_id"
 					v-if="playlists.length > 0"
 					v-model="playlists"
 					v-bind="dragOptions"
@@ -223,86 +231,85 @@
 					@end="drag = false"
 					@change="savePlaylistOrder"
 				>
-					<transition-group
-						type="transition"
-						:name="!drag ? 'draggable-list-transition' : null"
-					>
-						<playlist-item
-							class="item-draggable"
-							v-for="playlist in playlists"
-							:key="playlist._id"
-							:playlist="playlist"
-						>
-							<div slot="actions">
-								<i
-									v-if="isExcluded(playlist._id)"
-									class="material-icons stop-icon"
-									content="This playlist is blacklisted in this station"
-									v-tippy="{ theme: 'info' }"
-									>play_disabled</i
-								>
-								<i
-									v-if="
-										station.type === 'community' &&
-											(isOwnerOrAdmin() ||
-												station.partyMode) &&
-											!isSelected(playlist._id) &&
-											!isExcluded(playlist._id)
-									"
-									@click="selectPlaylist(playlist)"
-									class="material-icons play-icon"
-									:content="
-										station.partyMode
-											? 'Request songs from this playlist'
-											: 'Play songs from this playlist'
-									"
-									v-tippy
-									>play_arrow</i
-								>
-								<confirm
-									v-if="
-										station.type === 'community' &&
-											(isOwnerOrAdmin() ||
-												station.partyMode) &&
-											isSelected(playlist._id)
-									"
-									@confirm="deselectPlaylist(playlist._id)"
-								>
+					<template #item="{element}">
+						<div class="menu-list scrollable-list">
+							<playlist-item
+								class="item-draggable"
+								:playlist="element"
+							>
+								<template #actions>
 									<i
+										v-if="isExcluded(element._id)"
 										class="material-icons stop-icon"
+										content="This playlist is blacklisted in this station"
+										v-tippy="{ theme: 'info' }"
+										>play_disabled</i
+									>
+									<i
+										v-if="
+											station.type === 'community' &&
+												(isOwnerOrAdmin() ||
+													station.partyMode) &&
+												!isSelected(element._id) &&
+												!isExcluded(element._id)
+										"
+										@click="selectPlaylist(element)"
+										class="material-icons play-icon"
 										:content="
 											station.partyMode
-												? 'Stop requesting songs from this playlist'
-												: 'Stop playing songs from this playlist'
+												? 'Request songs from this playlist'
+												: 'Play songs from this playlist'
 										"
 										v-tippy
-										>stop</i
+										>play_arrow</i
 									>
-								</confirm>
-								<confirm
-									v-if="
-										isOwnerOrAdmin() &&
-											!isExcluded(playlist._id)
-									"
-									@confirm="blacklistPlaylist(playlist._id)"
-								>
+									<confirm
+										v-if="
+											station.type === 'community' &&
+												(isOwnerOrAdmin() ||
+													station.partyMode) &&
+												isSelected(element._id)
+										"
+										@confirm="deselectPlaylist(element._id)"
+									>
+										<i
+											class="material-icons stop-icon"
+											:content="
+												station.partyMode
+													? 'Stop requesting songs from this playlist'
+													: 'Stop playing songs from this playlist'
+											"
+											v-tippy
+											>stop</i
+										>
+									</confirm>
+									<confirm
+										v-if="
+											isOwnerOrAdmin() &&
+												!isExcluded(element._id)
+										"
+										@confirm="
+											blacklistPlaylist(element._id)
+										"
+									>
+										<i
+											class="material-icons stop-icon"
+											content="Blacklist Playlist"
+											v-tippy
+											>block</i
+										>
+									</confirm>
 									<i
-										class="material-icons stop-icon"
-										content="Blacklist Playlist"
+										@click="showPlaylist(element._id)"
+										class="material-icons edit-icon"
+										content="Edit Playlist"
 										v-tippy
-										>block</i
+										>edit</i
 									>
-								</confirm>
-								<i
-									@click="showPlaylist(playlist._id)"
-									class="material-icons edit-icon"
-									content="Edit Playlist"
-									v-tippy
-									>edit</i
-								>
-							</div>
-						</playlist-item>
-					</transition-group>
+								</template>
+							</playlist-item>
+						</div>
+					</template>
 				</draggable>
 				<p v-else class="has-text-centered scrollable-list">
 					You don't have any playlists!

+ 14 - 12
frontend/src/components/modals/ManageStationOwen/Tabs/Search.vue

@@ -25,16 +25,18 @@
 					:key="song._id"
 					:song="song"
 				>
-					<div class="song-actions" slot="actions">
-						<i
-							class="material-icons add-to-queue-icon"
-							v-if="station.partyMode && !station.locked"
-							@click="addSongToQueue(song.youtubeId)"
-							content="Add Song to Queue"
-							v-tippy
-							>queue</i
-						>
-					</div>
+					<template #actions>
+						<div class="song-actions">
+							<i
+								class="material-icons add-to-queue-icon"
+								v-if="station.partyMode && !station.locked"
+								@click="addSongToQueue(song.youtubeId)"
+								content="Add Song to Queue"
+								v-tippy
+								>queue</i
+							>
+						</div>
+					</template>
 				</song-item>
 				<button
 					v-if="resultsLeftCount > 0"
@@ -72,7 +74,7 @@
 					:key="result.id"
 					:result="result"
 				>
-					<div slot="actions">
+					<template #actions>
 						<transition name="search-query-actions" mode="out-in">
 							<a
 								class="button is-success"
@@ -98,7 +100,7 @@
 								Add to queue
 							</a>
 						</transition>
-					</div>
+					</template>
 				</search-query-item>
 
 				<a

+ 185 - 172
frontend/src/components/modals/ManageStationOwen/Tabs/Settings.vue

@@ -42,119 +42,121 @@
 		<div class="settings-buttons">
 			<div class="small-section">
 				<label class="label">Theme</label>
-				<tippy
-					class="button-wrapper"
-					theme="addToPlaylist"
-					interactive="true"
-					touch="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button :class="station.theme">
 							<i class="material-icons">palette</i>
 							{{ station.theme }}
 						</button>
-					</template>
-					<button
-						class="blue"
-						v-if="station.theme !== 'blue'"
-						@click="updateTheme('blue')"
-					>
-						<i class="material-icons">palette</i>
-						Blue
-					</button>
-					<button
-						class="purple"
-						v-if="station.theme !== 'purple'"
-						@click="updateTheme('purple')"
-					>
-						<i class="material-icons">palette</i>
-						Purple
-					</button>
-					<button
-						class="teal"
-						v-if="station.theme !== 'teal'"
-						@click="updateTheme('teal')"
-					>
-						<i class="material-icons">palette</i>
-						Teal
-					</button>
-					<button
-						class="orange"
-						v-if="station.theme !== 'orange'"
-						@click="updateTheme('orange')"
-					>
-						<i class="material-icons">palette</i>
-						Orange
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="blue"
+								v-if="station.theme !== 'blue'"
+								@click="updateTheme('blue')"
+							>
+								<i class="material-icons">palette</i>
+								Blue
+							</button>
+							<button
+								class="purple"
+								v-if="station.theme !== 'purple'"
+								@click="updateTheme('purple')"
+							>
+								<i class="material-icons">palette</i>
+								Purple
+							</button>
+							<button
+								class="teal"
+								v-if="station.theme !== 'teal'"
+								@click="updateTheme('teal')"
+							>
+								<i class="material-icons">palette</i>
+								Teal
+							</button>
+							<button
+								class="orange"
+								v-if="station.theme !== 'orange'"
+								@click="updateTheme('orange')"
+							>
+								<i class="material-icons">palette</i>
+								Orange
+							</button>
+						</template>
+					</tippy>
+				</div>
 			</div>
 			<div class="small-section">
 				<label class="label">Privacy</label>
-				<tippy
-					class="button-wrapper"
-					theme="addToPlaylist"
-					interactive="true"
-					touch="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button :class="privacyButtons[station.privacy].style">
 							<i class="material-icons">{{
 								privacyButtons[station.privacy].iconName
 							}}</i>
 							{{ station.privacy }}
 						</button>
-					</template>
-					<button
-						class="green"
-						v-if="station.privacy !== 'public'"
-						@click="updatePrivacy('public')"
-					>
-						<i class="material-icons">{{
-							privacyButtons["public"].iconName
-						}}</i>
-						Public
-					</button>
-					<button
-						class="orange"
-						v-if="station.privacy !== 'unlisted'"
-						@click="updatePrivacy('unlisted')"
-					>
-						<i class="material-icons">{{
-							privacyButtons["unlisted"].iconName
-						}}</i>
-						Unlisted
-					</button>
-					<button
-						class="red"
-						v-if="station.privacy !== 'private'"
-						@click="updatePrivacy('private')"
-					>
-						<i class="material-icons">{{
-							privacyButtons["private"].iconName
-						}}</i>
-						Private
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="green"
+								v-if="station.privacy !== 'public'"
+								@click="updatePrivacy('public')"
+							>
+								<i class="material-icons">{{
+									privacyButtons["public"].iconName
+								}}</i>
+								Public
+							</button>
+							<button
+								class="orange"
+								v-if="station.privacy !== 'unlisted'"
+								@click="updatePrivacy('unlisted')"
+							>
+								<i class="material-icons">{{
+									privacyButtons["unlisted"].iconName
+								}}</i>
+								Unlisted
+							</button>
+							<button
+								class="red"
+								v-if="station.privacy !== 'private'"
+								@click="updatePrivacy('private')"
+							>
+								<i class="material-icons">{{
+									privacyButtons["private"].iconName
+								}}</i>
+								Private
+							</button>
+						</template>
+					</tippy>
+				</div>
 			</div>
 			<div class="small-section">
 				<label class="label">Station Mode</label>
-				<tippy
-					v-if="station.type === 'community'"
-					class="button-wrapper"
-					theme="addToPlaylist"
-					touch="true"
-					interactive="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper" v-if="station.type === 'community'">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button
 							:class="{
 								blue: !station.partyMode,
@@ -168,24 +170,27 @@
 							}}</i>
 							{{ station.partyMode ? "Party" : "Playlist" }}
 						</button>
-					</template>
-					<button
-						class="blue"
-						v-if="station.partyMode"
-						@click="updatePartyMode(false)"
-					>
-						<i class="material-icons">playlist_play</i>
-						Playlist
-					</button>
-					<button
-						class="yellow"
-						v-if="!station.partyMode"
-						@click="updatePartyMode(true)"
-					>
-						<i class="material-icons">emoji_people</i>
-						Party
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="blue"
+								v-if="station.partyMode"
+								@click="updatePartyMode(false)"
+							>
+								<i class="material-icons">playlist_play</i>
+								Playlist
+							</button>
+							<button
+								class="yellow"
+								v-if="!station.partyMode"
+								@click="updatePartyMode(true)"
+							>
+								<i class="material-icons">emoji_people</i>
+								Party
+							</button>
+						</template>
+					</tippy>
+				</div>
 				<div v-else class="button-wrapper">
 					<button
 						class="blue"
@@ -199,17 +204,15 @@
 			</div>
 			<div v-if="!station.partyMode" class="small-section">
 				<label class="label">Play Mode</label>
-				<tippy
-					v-if="station.type === 'community'"
-					class="button-wrapper"
-					theme="addToPlaylist"
-					touch="true"
-					interactive="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper" v-if="station.type === 'community'">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button class="blue">
 							<i class="material-icons">{{
 								station.playMode === "random"
@@ -222,24 +225,32 @@
 									: "Sequential"
 							}}
 						</button>
-					</template>
-					<button
-						class="blue"
-						v-if="station.playMode === 'sequential'"
-						@click="updatePlayMode('random')"
-					>
-						<i class="material-icons">shuffle</i>
-						Random
-					</button>
-					<button
-						class="blue"
-						v-if="station.playMode === 'random'"
-						@click="updatePlayMode('sequential')"
-					>
-						<i class="material-icons">format_list_numbered</i>
-						Sequential
-					</button>
-				</tippy>
+
+						<template #content>
+							<div class="button-wrapper">
+								<button
+									class="blue"
+									v-if="station.playMode === 'sequential'"
+									@click="updatePlayMode('random')"
+								>
+									<i class="material-icons">shuffle</i>
+									Random
+								</button>
+								<button
+									class="blue"
+									v-if="station.playMode === 'random'"
+									@click="updatePlayMode('sequential')"
+								>
+									<i class="material-icons"
+										>format_list_numbered</i
+									>
+									Sequential
+								</button>
+							</div>
+						</template>
+					</tippy>
+				</div>
+
 				<div v-else class="button-wrapper">
 					<button
 						class="blue"
@@ -258,16 +269,15 @@
 				class="small-section"
 			>
 				<label class="label">Queue lock</label>
-				<tippy
-					class="button-wrapper"
-					theme="addToPlaylist"
-					interactive="true"
-					touch="true"
-					placement="bottom"
-					trigger="click"
-					append-to="parent"
-				>
-					<template #trigger>
+				<div class="button-wrapper">
+					<tippy
+						theme="addToPlaylist"
+						:interactive="true"
+						:touch="true"
+						placement="bottom"
+						trigger="click"
+						append-to="parent"
+					>
 						<button
 							:class="{
 								green: station.locked,
@@ -279,24 +289,27 @@
 							}}</i>
 							{{ station.locked ? "Locked" : "Unlocked" }}
 						</button>
-					</template>
-					<button
-						class="green"
-						v-if="!station.locked"
-						@click="updateQueueLock(true)"
-					>
-						<i class="material-icons">lock</i>
-						Locked
-					</button>
-					<button
-						class="red"
-						v-if="station.locked"
-						@click="updateQueueLock(false)"
-					>
-						<i class="material-icons">lock_open</i>
-						Unlocked
-					</button>
-				</tippy>
+
+						<template #content>
+							<button
+								class="green"
+								v-if="!station.locked"
+								@click="updateQueueLock(true)"
+							>
+								<i class="material-icons">lock</i>
+								Locked
+							</button>
+							<button
+								class="red"
+								v-if="station.locked"
+								@click="updateQueueLock(false)"
+							>
+								<i class="material-icons">lock_open</i>
+								Unlocked
+							</button>
+						</template>
+					</tippy>
+				</div>
 			</div>
 		</div>
 	</div>

+ 1 - 1
frontend/src/components/modals/ManageStationOwen/index.vue

@@ -449,7 +449,7 @@ export default {
 			{ modal: "manageStation" }
 		);
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		this.socket.dispatch(
 			"apis.leaveRoom",
 			`manage-station.${this.stationId}`,

+ 4 - 4
frontend/src/components/modals/Report.vue

@@ -1,6 +1,6 @@
 <template>
 	<modal title="Report">
-		<div slot="body">
+		<template #body>
 			<div class="edit-report-wrapper">
 				<song-item
 					:song="localSong"
@@ -42,8 +42,8 @@
 					</div>
 				</div>
 			</div>
-		</div>
-		<div slot="footer">
+		</template>
+		<template #footer>
 			<a class="button is-success" @click="create()" href="#">
 				<i class="material-icons save-changes">done</i>
 				<span>&nbsp;Create</span>
@@ -51,7 +51,7 @@
 			<a class="button is-danger" href="#" @click="closeModal('report')">
 				<span>&nbsp;Cancel</span>
 			</a>
-		</div>
+		</template>
 	</modal>
 </template>
 

+ 4 - 4
frontend/src/components/modals/RequestSong.vue

@@ -1,6 +1,6 @@
 <template>
 	<modal title="Request Song">
-		<div slot="body">
+		<template #body>
 			<div class="vertical-padding">
 				<!-- Choosing a song from youtube -->
 
@@ -45,7 +45,7 @@
 						:key="result.id"
 						:result="result"
 					>
-						<div slot="actions">
+						<template #actions>
 							<transition
 								name="search-query-actions"
 								mode="out-in"
@@ -76,7 +76,7 @@
 									Add to queue
 								</a>
 							</transition>
-						</div>
+						</template>
 					</search-query-item>
 
 					<a
@@ -135,7 +135,7 @@
 					</div>
 				</div>
 			</div>
-		</div>
+		</template>
 	</modal>
 </template>
 

+ 2 - 3
frontend/src/components/modals/ViewPunishment.vue

@@ -1,7 +1,7 @@
 <template>
 	<div>
 		<modal title="View Punishment">
-			<div slot="body" v-if="punishment && punishment._id">
+			<template #body v-if="punishment && punishment._id">
 				<article class="message">
 					<div class="message-body">
 						<strong>Type:</strong>
@@ -54,8 +54,7 @@
 						<br />
 					</div>
 				</article>
-			</div>
-			<div slot="footer"></div>
+			</template>
 		</modal>
 	</div>
 </template>

+ 4 - 4
frontend/src/components/modals/ViewReport.vue

@@ -1,6 +1,6 @@
 <template>
 	<modal title="View Report">
-		<div slot="body" v-if="report && report._id">
+		<template #body v-if="report && report._id">
 			<router-link
 				v-if="$route.query.returnToSong"
 				class="button is-dark back-to-song"
@@ -63,8 +63,8 @@
 					</tr>
 				</tbody>
 			</table>
-		</div>
-		<div slot="footer" v-if="report && report._id">
+		</template>
+		<template #footer v-if="report && report._id">
 			<a class="button is-primary" href="#" @click="resolve(report._id)">
 				<span>Resolve</span>
 			</a>
@@ -75,7 +75,7 @@
 			>
 				<span>Go to song</span>
 			</a>
-		</div>
+		</template>
 	</modal>
 </template>
 

+ 5 - 4
frontend/src/components/modals/WhatIsNew.vue

@@ -1,13 +1,13 @@
 <template>
 	<div v-if="news !== null">
 		<modal title="News" class="what-is-news-modal">
-			<div slot="body">
+			<template #body>
 				<div
 					class="section news-item"
 					v-html="sanitize(marked(news.markdown))"
 				></div>
-			</div>
-			<div slot="footer">
+			</template>
+			<template #footer>
 				<span v-if="news.createdBy">
 					By
 					<user-id-to-username
@@ -21,9 +21,10 @@
 						})
 					}}
 				</span>
-			</div>
+			</template>
 		</modal>
 	</div>
+	<div v-else></div>
 </template>
 
 <script>

+ 27 - 31
frontend/src/main.js

@@ -1,12 +1,13 @@
-import Vue from "vue";
+/* eslint-disable vue/one-component-per-file */
+import { createApp } from "vue";
 
-import VueTippy, { TippyComponent } from "vue-tippy";
-import VueRouter from "vue-router";
+import VueTippy, { Tippy } from "vue-tippy";
+import { createRouter, createWebHistory } from "vue-router";
 
 import ws from "@/ws";
 import store from "./store";
 
-import App from "./App.vue";
+import AppComponent from "./App.vue";
 
 const REQUIRED_CONFIG_VERSION = 5;
 
@@ -14,7 +15,11 @@ const handleMetadata = attrs => {
 	document.title = `Musare | ${attrs.title}`;
 };
 
-Vue.use(VueTippy, {
+const app = createApp(AppComponent);
+
+app.use(store);
+
+app.use(VueTippy, {
 	directive: "tippy", // => v-tippy
 	flipDuration: 0,
 	touch: false,
@@ -26,14 +31,12 @@ Vue.use(VueTippy, {
 		}
 	},
 	allowHTML: true,
-	animation: "scale",
-	theme: "dark",
-	arrow: true
+	defaultProps: { animation: "scale" }
 });
 
-Vue.component("Tippy", TippyComponent);
+app.component("Tippy", Tippy);
 
-Vue.component("Metadata", {
+app.component("Metadata", {
 	watch: {
 		$attrs: {
 			// eslint-disable-next-line vue/no-arrow-functions-in-watch
@@ -49,10 +52,8 @@ Vue.component("Metadata", {
 	}
 });
 
-Vue.use(VueRouter);
-
-Vue.directive("scroll", {
-	inserted(el, binding) {
+app.directive("scroll", {
+	mounted(el, binding) {
 		const f = evt => {
 			clearTimeout(window.scrollDebounceId);
 			window.scrollDebounceId = setTimeout(() => {
@@ -65,15 +66,15 @@ Vue.directive("scroll", {
 	}
 });
 
-Vue.directive("focus", {
-	inserted(el) {
+app.directive("focus", {
+	mounted(el) {
 		window.focusedElementBefore = document.activeElement;
 		el.focus();
 	}
 });
 
-const router = new VueRouter({
-	mode: "history",
+const router = createRouter({
+	history: createWebHistory(),
 	routes: [
 		{
 			path: "/",
@@ -81,7 +82,7 @@ const router = new VueRouter({
 		},
 		{
 			path: "/404",
-			alias: ["*"],
+			alias: ["/:pathMatch(.*)*"],
 			component: () => import("@/pages/404.vue")
 		},
 		{
@@ -164,6 +165,8 @@ const router = new VueRouter({
 	]
 });
 
+app.use(router);
+
 lofig.folder = "../config/default.json";
 
 (async () => {
@@ -275,22 +278,21 @@ lofig.folder = "../config/default.json";
 		} else next();
 	});
 
-	Vue.directive("click-outside", {
-		bind(element, binding) {
+	app.directive("click-outside", {
+		beforeMount(element, binding) {
 			window.handleOutsideClick = event => {
 				if (
 					!(
 						element === event.target ||
 						element.contains(event.target)
 					)
-				) {
+				)
 					binding.value();
-				}
 			};
 
 			document.body.addEventListener("click", window.handleOutsideClick);
 		},
-		unbind() {
+		unmounted() {
 			document.body.removeEventListener(
 				"click",
 				window.handleOutsideClick
@@ -298,11 +300,5 @@ lofig.folder = "../config/default.json";
 		}
 	});
 
-	// eslint-disable-next-line no-new
-	new Vue({
-		router,
-		store,
-		el: "#root",
-		render: wrapper => wrapper(App)
-	});
+	app.mount("#root");
 })();

+ 4 - 4
frontend/src/mixins/ScrollAndFetchHandler.vue

@@ -1,5 +1,8 @@
 <script>
 export default {
+	setup() {
+		window.addEventListener("scroll", this.handleScroll);
+	},
 	data() {
 		return {
 			position: 1,
@@ -20,10 +23,7 @@ export default {
 	unmounted() {
 		clearInterval(this.interval);
 	},
-	created() {
-		window.addEventListener("scroll", this.handleScroll);
-	},
-	destroyed() {
+	onUnmounted() {
 		window.removeEventListener("scroll", this.handleScroll);
 	},
 	methods: {

+ 20 - 11
frontend/src/pages/Admin/index.vue

@@ -133,22 +133,31 @@
 
 <script>
 import { mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 
 import MainHeader from "@/components/layout/MainHeader.vue";
 
 export default {
 	components: {
 		MainHeader,
-		UnverifiedSongs: () => import("./tabs/UnverifiedSongs.vue"),
-		VerifiedSongs: () => import("./tabs/VerifiedSongs.vue"),
-		HiddenSongs: () => import("./tabs/HiddenSongs.vue"),
-		Stations: () => import("./tabs/Stations.vue"),
-		Playlists: () => import("./tabs/Playlists.vue"),
-		Reports: () => import("./tabs/Reports.vue"),
-		News: () => import("./tabs/News.vue"),
-		Users: () => import("./tabs/Users.vue"),
-		Statistics: () => import("./tabs/Statistics.vue"),
-		Punishments: () => import("./tabs/Punishments.vue")
+		UnverifiedSongs: defineAsyncComponent(() =>
+			import("./tabs/UnverifiedSongs.vue")
+		),
+		VerifiedSongs: defineAsyncComponent(() =>
+			import("./tabs/VerifiedSongs.vue")
+		),
+		HiddenSongs: defineAsyncComponent(() =>
+			import("./tabs/HiddenSongs.vue")
+		),
+		Stations: defineAsyncComponent(() => import("./tabs/Stations.vue")),
+		Playlists: defineAsyncComponent(() => import("./tabs/Playlists.vue")),
+		Reports: defineAsyncComponent(() => import("./tabs/Reports.vue")),
+		News: defineAsyncComponent(() => import("./tabs/News.vue")),
+		Users: defineAsyncComponent(() => import("./tabs/Users.vue")),
+		Statistics: defineAsyncComponent(() => import("./tabs/Statistics.vue")),
+		Punishments: defineAsyncComponent(() =>
+			import("./tabs/Punishments.vue")
+		)
 	},
 	data() {
 		return {
@@ -166,7 +175,7 @@ export default {
 	mounted() {
 		this.changeTab(this.$route.path);
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		this.socket.dispatch("apis.leaveRooms", () => {});
 	},
 	methods: {

+ 5 - 2
frontend/src/pages/Admin/tabs/HiddenSongs.vue

@@ -5,7 +5,7 @@
 			<p>
 				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
 				<br />
-				<span>Loaded songs: {{ this.songs.length }}</span>
+				<span>Loaded songs: {{ songs.length }}</span>
 			</p>
 			<input
 				v-model="searchQuery"
@@ -172,6 +172,7 @@
 
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 
 import Toast from "toasters";
 
@@ -184,7 +185,9 @@ import ws from "@/ws";
 
 export default {
 	components: {
-		EditSong: () => import("@/components/modals/EditSong"),
+		EditSong: defineAsyncComponent(() =>
+			import("@/components/modals/EditSong")
+		),
 		UserIdToUsername,
 		FloatingBox
 	},

+ 4 - 1
frontend/src/pages/Admin/tabs/News.vue

@@ -56,6 +56,7 @@
 
 <script>
 import { mapActions, mapState, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 import Toast from "toasters";
 
 import ws from "@/ws";
@@ -67,7 +68,9 @@ export default {
 	components: {
 		Confirm,
 		UserIdToUsername,
-		EditNews: () => import("@/components/modals/EditNews.vue")
+		EditNews: defineAsyncComponent(() =>
+			import("@/components/modals/EditNews.vue")
+		)
 	},
 	data() {
 		return {

+ 10 - 3
frontend/src/pages/Admin/tabs/Playlists.vue

@@ -91,6 +91,7 @@
 
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 
 import Toast from "toasters";
 
@@ -101,10 +102,16 @@ import utils from "../../../../js/utils";
 
 export default {
 	components: {
-		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
+		EditPlaylist: defineAsyncComponent(() =>
+			import("@/components/modals/EditPlaylist")
+		),
 		UserIdToUsername,
-		Report: () => import("@/components/modals/Report.vue"),
-		EditSong: () => import("@/components/modals/EditSong")
+		Report: defineAsyncComponent(() =>
+			import("@/components/modals/Report.vue")
+		),
+		EditSong: defineAsyncComponent(() =>
+			import("@/components/modals/EditSong")
+		)
 	},
 	data() {
 		return {

+ 4 - 1
frontend/src/pages/Admin/tabs/Punishments.vue

@@ -96,12 +96,15 @@
 <script>
 import { mapState, mapGetters, mapActions } from "vuex";
 import Toast from "toasters";
+import { defineAsyncComponent } from "vue";
 
 import ws from "@/ws";
 
 export default {
 	components: {
-		ViewPunishment: () => import("@/components/modals/ViewPunishment.vue")
+		ViewPunishment: defineAsyncComponent(() =>
+			import("@/components/modals/ViewPunishment.vue")
+		)
 	},
 	data() {
 		return {

+ 4 - 1
frontend/src/pages/Admin/tabs/Reports.vue

@@ -73,6 +73,7 @@
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
 import { formatDistance } from "date-fns";
+import { defineAsyncComponent } from "vue";
 
 import Toast from "toasters";
 import UserIdToUsername from "@/components/UserIdToUsername.vue";
@@ -80,7 +81,9 @@ import ws from "@/ws";
 
 export default {
 	components: {
-		ViewReport: () => import("@/components/modals/ViewReport.vue"),
+		ViewReport: defineAsyncComponent(() =>
+			import("@/components/modals/ViewReport.vue")
+		),
 		UserIdToUsername
 	},
 	data() {

+ 22 - 9
frontend/src/pages/Admin/tabs/Stations.vue

@@ -197,6 +197,7 @@
 
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 
 import Toast from "toasters";
 import UserIdToUsername from "@/components/UserIdToUsername.vue";
@@ -205,15 +206,27 @@ import ws from "@/ws";
 
 export default {
 	components: {
-		RequestSong: () => import("@/components/modals/RequestSong.vue"),
-		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
-		CreatePlaylist: () => import("@/components/modals/CreatePlaylist.vue"),
-		ManageStationOwen: () =>
-			import("@/components/modals/ManageStationOwen/index.vue"),
-		ManageStationKris: () =>
-			import("@/components/modals/ManageStationKris/index.vue"),
-		Report: () => import("@/components/modals/Report.vue"),
-		EditSong: () => import("@/components/modals/EditSong"),
+		RequestSong: defineAsyncComponent(() =>
+			import("@/components/modals/RequestSong.vue")
+		),
+		EditPlaylist: defineAsyncComponent(() =>
+			import("@/components/modals/EditPlaylist")
+		),
+		CreatePlaylist: defineAsyncComponent(() =>
+			import("@/components/modals/CreatePlaylist.vue")
+		),
+		ManageStationOwen: defineAsyncComponent(() =>
+			import("@/components/modals/ManageStationOwen/index.vue")
+		),
+		ManageStationKris: defineAsyncComponent(() =>
+			import("@/components/modals/ManageStationKris/index.vue")
+		),
+		Report: defineAsyncComponent(() =>
+			import("@/components/modals/Report.vue")
+		),
+		EditSong: defineAsyncComponent(() =>
+			import("@/components/modals/EditSong")
+		),
 		UserIdToUsername,
 		Confirm
 	},

+ 5 - 2
frontend/src/pages/Admin/tabs/UnverifiedSongs.vue

@@ -5,7 +5,7 @@
 			<p>
 				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
 				<br />
-				<span>Loaded songs: {{ this.songs.length }}</span>
+				<span>Loaded songs: {{ songs.length }}</span>
 			</p>
 			<input
 				v-model="searchQuery"
@@ -183,6 +183,7 @@
 
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 
 import Toast from "toasters";
 
@@ -196,7 +197,9 @@ import ws from "@/ws";
 
 export default {
 	components: {
-		EditSong: () => import("@/components/modals/EditSong"),
+		EditSong: defineAsyncComponent(() =>
+			import("@/components/modals/EditSong")
+		),
 		UserIdToUsername,
 		FloatingBox,
 		Confirm

+ 4 - 1
frontend/src/pages/Admin/tabs/Users.vue

@@ -101,6 +101,7 @@
 
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 import Toast from "toasters";
 
 import ProfilePicture from "@/components/ProfilePicture.vue";
@@ -108,7 +109,9 @@ import ws from "@/ws";
 
 export default {
 	components: {
-		EditUser: () => import("@/components/modals/EditUser.vue"),
+		EditUser: defineAsyncComponent(() =>
+			import("@/components/modals/EditUser.vue")
+		),
 		ProfilePicture
 	},
 	data() {

+ 6 - 3
frontend/src/pages/Admin/tabs/VerifiedSongs.vue

@@ -5,7 +5,7 @@
 			<p>
 				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
 				<br />
-				<span>Loaded songs: {{ this.songs.length }}</span>
+				<span>Loaded songs: {{ songs.length }}</span>
 			</p>
 			<input
 				v-model="searchQuery"
@@ -240,6 +240,7 @@
 
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 
 import Toast from "toasters";
 
@@ -255,7 +256,9 @@ import ws from "@/ws";
 
 export default {
 	components: {
-		EditSong: () => import("@/components/modals/EditSong"),
+		EditSong: defineAsyncComponent(() =>
+			import("@/components/modals/EditSong")
+		),
 		UserIdToUsername,
 		FloatingBox,
 		Confirm
@@ -406,7 +409,7 @@ export default {
 			}
 		);
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		const shortcutNames = [
 			"verifiedSongs.toggleKeyboardShortcutsHelper",
 			"verifiedSongs.resetKeyboardShortcutsHelper"

+ 63 - 59
frontend/src/pages/Home.vue

@@ -34,7 +34,7 @@
 					</div>
 				</div>
 			</div>
-			<div v-if="favoriteStations.length > 0" class="group">
+			<div class="group" v-show="favoriteStations.length > 0">
 				<div class="group-title">
 					<div>
 						<h2>My Favorites</h2>
@@ -42,48 +42,47 @@
 				</div>
 
 				<draggable
-					class="scrollable-list"
+					tag="transition-group"
+					:component-data="{
+						name: !drag ? 'draggable-list-transition' : null
+					}"
+					item-key="_id"
 					v-model="favoriteStations"
 					v-bind="dragOptions"
 					@start="drag = true"
 					@end="drag = false"
 					@change="changeFavoriteOrder"
 				>
-					<transition-group
-						type="transition"
-						:name="!drag ? 'draggable-list-transition' : null"
-					>
+					<template #item="{element}">
 						<router-link
-							v-for="station in favoriteStations"
-							:key="`key-${station._id}`"
 							:to="{
 								name: 'station',
-								params: { id: station.name }
+								params: { id: element.name }
 							}"
 							:class="{
 								card: true,
 								'station-card': true,
 								'item-draggable': true,
-								isPrivate: station.privacy === 'private',
-								isMine: isOwner(station)
+								isPrivate: element.privacy === 'private',
+								isMine: isOwner(element)
 							}"
 							:style="
-								'--primary-color: var(--' + station.theme + ')'
+								'--primary-color: var(--' + element.theme + ')'
 							"
 						>
 							<song-thumbnail
 								class="card-image"
-								:song="station.currentSong"
+								:song="element.currentSong"
 							/>
 							<div class="card-content">
 								<div class="media">
 									<div class="media-left displayName">
 										<i
 											v-if="
-												loggedIn && !station.isFavorited
+												loggedIn && !element.isFavorited
 											"
 											@click.prevent="
-												favoriteStation(station)
+												favoriteStation(element._id)
 											"
 											class="favorite material-icons"
 											content="Favorite Station"
@@ -92,19 +91,19 @@
 										>
 										<i
 											v-if="
-												loggedIn && station.isFavorited
+												loggedIn && element.isFavorited
 											"
 											@click.prevent="
-												unfavoriteStation(station)
+												unfavoriteStation(element._id)
 											"
 											class="favorite material-icons"
 											content="Unfavorite Station"
 											v-tippy
 											>star</i
 										>
-										<h5>{{ station.displayName }}</h5>
+										<h5>{{ element.displayName }}</h5>
 										<i
-											v-if="station.type === 'official'"
+											v-if="element.type === 'official'"
 											class="material-icons verified-station"
 											content="Verified Station"
 											v-tippy="{
@@ -117,7 +116,7 @@
 								</div>
 
 								<div class="content">
-									{{ station.description }}
+									{{ element.description }}
 								</div>
 								<div class="under-content">
 									<p class="hostedBy">
@@ -125,14 +124,14 @@
 										<span class="host">
 											<span
 												v-if="
-													station.type === 'official'
+													element.type === 'official'
 												"
 												title="Musare"
 												>Musare</span
 											>
 											<user-id-to-username
 												v-else
-												:user-id="station.owner"
+												:user-id="element.owner"
 												:link="true"
 											/>
 										</span>
@@ -140,8 +139,8 @@
 									<div class="icons">
 										<i
 											v-if="
-												station.type === 'community' &&
-													isOwner(station)
+												element.type === 'community' &&
+													isOwner(element)
 											"
 											class="homeIcon material-icons"
 											content="This is your station."
@@ -149,7 +148,7 @@
 											>home</i
 										>
 										<i
-											v-if="station.privacy === 'private'"
+											v-if="element.privacy === 'private'"
 											class="privateIcon material-icons"
 											content="This station is not visible to other users."
 											v-tippy="{ theme: 'info' }"
@@ -157,7 +156,7 @@
 										>
 										<i
 											v-if="
-												station.privacy === 'unlisted'
+												element.privacy === 'unlisted'
 											"
 											class="unlistedIcon material-icons"
 											content="Unlisted Station"
@@ -170,8 +169,8 @@
 							<div class="bottomBar">
 								<i
 									v-if="
-										station.paused &&
-											station.currentSong.title
+										element.paused &&
+											element.currentSong.title
 									"
 									class="material-icons"
 									content="Station Paused"
@@ -179,30 +178,30 @@
 									>pause</i
 								>
 								<i
-									v-else-if="station.currentSong.title"
+									v-else-if="element.currentSong.title"
 									class="material-icons"
 									>music_note</i
 								>
 								<i v-else class="material-icons">music_off</i>
 								<span
-									v-if="station.currentSong.title"
+									v-if="element.currentSong.title"
 									class="songTitle"
 									:title="
-										station.currentSong.artists.length > 0
+										element.currentSong.artists.length > 0
 											? 'Now Playing: ' +
-											  station.currentSong.title +
+											  element.currentSong.title +
 											  ' by ' +
-											  station.currentSong.artists.join(
+											  element.currentSong.artists.join(
 													','
 											  )
 											: 'Now Playing: ' +
-											  station.currentSong.title
+											  element.currentSong.title
 									"
-									>{{ station.currentSong.title }}
+									>{{ element.currentSong.title }}
 									{{
-										station.currentSong.artists.length > 0
+										element.currentSong.artists.length > 0
 											? " by " +
-											  station.currentSong.artists.join(
+											  element.currentSong.artists.join(
 													","
 											  )
 											: ""
@@ -214,20 +213,20 @@
 								<i
 									class="material-icons stationMode"
 									:content="
-										station.partyMode
+										element.partyMode
 											? 'Station in Party mode'
 											: 'Station in Playlist mode'
 									"
 									v-tippy="{ theme: 'info' }"
 									>{{
-										station.partyMode
+										element.partyMode
 											? "emoji_people"
 											: "playlist_play"
 									}}</i
 								>
 							</div>
 						</router-link>
-					</transition-group>
+					</template>
 				</draggable>
 			</div>
 			<div class="group bottom">
@@ -304,7 +303,9 @@
 							<div class="media-left displayName">
 								<i
 									v-if="loggedIn && !station.isFavorited"
-									@click.prevent="favoriteStation(station)"
+									@click.prevent="
+										favoriteStation(station._id)
+									"
 									class="favorite material-icons"
 									content="Favorite Station"
 									v-tippy
@@ -312,7 +313,9 @@
 								>
 								<i
 									v-if="loggedIn && station.isFavorited"
-									@click.prevent="unfavoriteStation(station)"
+									@click.prevent="
+										unfavoriteStation(station._id)
+									"
 									class="favorite material-icons"
 									content="Unfavorite Station"
 									v-tippy
@@ -440,6 +443,7 @@
 
 <script>
 import { mapState, mapGetters, mapActions } from "vuex";
+import { defineAsyncComponent } from "vue";
 import draggable from "vuedraggable";
 import Toast from "toasters";
 
@@ -455,8 +459,9 @@ export default {
 		MainHeader,
 		MainFooter,
 		SongThumbnail,
-		CreateCommunityStation: () =>
-			import("@/components/modals/CreateCommunityStation.vue"),
+		CreateCommunityStation: defineAsyncComponent(() =>
+			import("@/components/modals/CreateCommunityStation.vue")
+		),
 		UserIdToUsername,
 		draggable
 	},
@@ -509,8 +514,11 @@ export default {
 		}
 	},
 	watch: {
-		orderOfFavoriteStations() {
-			this.calculateFavoriteStations();
+		orderOfFavoriteStations: {
+			deep: true,
+			handler() {
+				this.calculateFavoriteStations();
+			}
 		}
 	},
 	async mounted() {
@@ -681,7 +689,7 @@ export default {
 			this.orderOfFavoriteStations = res.data.order;
 		});
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		this.socket.dispatch("apis.leaveRoom", "home", () => {});
 	},
 	methods: {
@@ -719,21 +727,17 @@ export default {
 		isPlaying(station) {
 			return typeof station.currentSong.title !== "undefined";
 		},
-		favoriteStation(station) {
-			this.socket.dispatch(
-				"stations.favoriteStation",
-				station._id,
-				res => {
-					if (res.status === "success") {
-						new Toast("Successfully favorited station.");
-					} else new Toast(res.message);
-				}
-			);
+		favoriteStation(stationId) {
+			this.socket.dispatch("stations.favoriteStation", stationId, res => {
+				if (res.status === "success") {
+					new Toast("Successfully favorited station.");
+				} else new Toast(res.message);
+			});
 		},
-		unfavoriteStation(station) {
+		unfavoriteStation(stationId) {
 			this.socket.dispatch(
 				"stations.unfavoriteStation",
-				station._id,
+				stationId,
 				res => {
 					if (res.status === "success") {
 						new Toast("Successfully unfavorited station.");

+ 38 - 39
frontend/src/pages/Profile/Tabs/Playlists.vue

@@ -21,55 +21,51 @@
 			<hr class="section-horizontal-rule" />
 
 			<draggable
-				class="menu-list scrollable-list"
+				tag="transition-group"
+				:component-data="{
+					name: !drag ? 'draggable-list-transition' : null
+				}"
 				v-if="playlists.length > 0"
 				v-model="playlists"
+				item-key="_id"
 				v-bind="dragOptions"
 				@start="drag = true"
 				@end="drag = false"
 				@change="savePlaylistOrder"
 			>
-				<transition-group
-					type="transition"
-					:name="!drag ? 'draggable-list-transition' : null"
-				>
-					<div
+				<template #item="{element}">
+					<playlist-item
+						v-if="
+							element.privacy === 'public' ||
+								(element.privacy === 'private' &&
+									element.createdBy === userId)
+						"
+						:playlist="element"
 						:class="{
 							item: true,
 							'item-draggable': myUserId === userId
 						}"
-						v-for="playlist in playlists"
-						:key="playlist._id"
 					>
-						<playlist-item
-							v-if="
-								playlist.privacy === 'public' ||
-									(playlist.privacy === 'private' &&
-										playlist.createdBy === userId)
-							"
-							:playlist="playlist"
-						>
-							<div slot="actions">
-								<i
-									v-if="myUserId === userId"
-									@click="showPlaylist(playlist._id)"
-									class="material-icons edit-icon"
-									content="Edit Playlist"
-									v-tippy
-									>edit</i
-								>
-								<i
-									v-else
-									@click="showPlaylist(playlist._id)"
-									class="material-icons view-icon"
-									content="View Playlist"
-									v-tippy
-									>visibility</i
-								>
-							</div>
-						</playlist-item>
-					</div>
-				</transition-group>
+						<template #actions>
+							<i
+								v-if="myUserId === userId"
+								@click="showPlaylist(element._id)"
+								class="material-icons edit-icon"
+								content="Edit Playlist"
+								v-tippy
+								>edit</i
+							>
+							<i
+								v-else
+								@click="showPlaylist(element._id)"
+								class="material-icons view-icon"
+								content="View Playlist"
+								v-tippy
+								>visibility</i
+							>
+						</template>
+					</playlist-item>
+				</template>
 			</draggable>
 
 			<button
@@ -89,6 +85,7 @@
 
 <script>
 import { mapActions, mapState, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 
 import PlaylistItem from "@/components/PlaylistItem.vue";
 import SortablePlaylists from "@/mixins/SortablePlaylists.vue";
@@ -97,7 +94,9 @@ import ws from "@/ws";
 export default {
 	components: {
 		PlaylistItem,
-		CreatePlaylist: () => import("@/components/modals/CreatePlaylist.vue")
+		CreatePlaylist: defineAsyncComponent(() =>
+			import("@/components/modals/CreatePlaylist.vue")
+		)
 	},
 	mixins: [SortablePlaylists],
 	props: {
@@ -142,7 +141,7 @@ export default {
 			this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
 		});
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		this.socket.dispatch(
 			"apis.leaveRoom",
 			`profile.${this.userId}.playlists`,

+ 4 - 3
frontend/src/pages/Profile/Tabs/RecentActivity.vue

@@ -18,10 +18,11 @@
 					:key="activity._id"
 					:activity="activity"
 				>
-					<div slot="actions">
+					<template #actions>
 						<confirm
 							v-if="userId === myUserId"
 							@confirm="hideActivity(activity._id)"
+							placement="left"
 						>
 							<a content="Hide Activity" v-tippy>
 								<i class="material-icons hide-icon"
@@ -29,7 +30,7 @@
 								>
 							</a>
 						</confirm>
-					</div>
+					</template>
 				</activity-item>
 			</div>
 		</div>
@@ -123,7 +124,7 @@ export default {
 			this.offsettedFromNextSet = 0;
 		});
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		this.socket.dispatch(
 			"apis.leaveRoom",
 			`profile.${this.userId}.activities`,

+ 12 - 5
frontend/src/pages/Profile/index.vue

@@ -107,13 +107,14 @@
 <script>
 import { mapState, mapGetters } from "vuex";
 import { format, parseISO } from "date-fns";
+import { defineAsyncComponent } from "vue";
+
+import TabQueryHandler from "@/mixins/TabQueryHandler.vue";
 
 import ProfilePicture from "@/components/ProfilePicture";
 import MainHeader from "@/components/layout/MainHeader";
 import MainFooter from "@/components/layout/MainFooter.vue";
 
-import TabQueryHandler from "@/mixins/TabQueryHandler.vue";
-
 import RecentActivity from "./Tabs/RecentActivity.vue";
 import Playlists from "./Tabs/Playlists.vue";
 
@@ -124,9 +125,15 @@ export default {
 		ProfilePicture,
 		RecentActivity,
 		Playlists,
-		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
-		Report: () => import("@/components/modals/Report.vue"),
-		EditSong: () => import("@/components/modals/EditSong")
+		EditPlaylist: defineAsyncComponent(() =>
+			import("@/components/modals/EditPlaylist")
+		),
+		Report: defineAsyncComponent(() =>
+			import("@/components/modals/Report.vue")
+		),
+		EditSong: defineAsyncComponent(() =>
+			import("@/components/modals/EditSong")
+		)
 	},
 	mixins: [TabQueryHandler],
 	data() {

+ 2 - 2
frontend/src/pages/ResetPassword.vue

@@ -18,7 +18,7 @@
 					<p class="step" :class="{ selected: step === 3 }">3</p>
 				</div>
 
-				<transition name="steps-fade" mode="out-in">
+				<transition-group name="steps-fade" mode="out-in">
 					<!-- Step 1 -- Enter email address -->
 					<div class="content-box" v-if="step === 1" :key="step">
 						<h2 class="content-box-title">
@@ -257,7 +257,7 @@
 							>Return to Settings</router-link
 						>
 					</div>
-				</transition>
+				</transition-group>
 			</div>
 		</div>
 		<main-footer />

+ 19 - 6
frontend/src/pages/Settings/index.vue

@@ -49,21 +49,34 @@
 
 <script>
 import { mapActions, mapGetters, mapState } from "vuex";
+import { defineAsyncComponent } from "vue";
+
 import Toast from "toasters";
 
+import TabQueryHandler from "@/mixins/TabQueryHandler.vue";
+
 import MainHeader from "@/components/layout/MainHeader.vue";
 import MainFooter from "@/components/layout/MainFooter.vue";
-import TabQueryHandler from "@/mixins/TabQueryHandler.vue";
 
 export default {
 	components: {
 		MainHeader,
 		MainFooter,
-		SecuritySettings: () => import("./Tabs/Security.vue"),
-		AccountSettings: () => import("./Tabs/Account.vue"),
-		ProfileSettings: () => import("./Tabs/Profile.vue"),
-		PreferencesSettings: () => import("./Tabs/Preferences.vue"),
-		RemoveAccount: () => import("@/components/modals/RemoveAccount.vue")
+		SecuritySettings: defineAsyncComponent(() =>
+			import("./Tabs/Security.vue")
+		),
+		AccountSettings: defineAsyncComponent(() =>
+			import("./Tabs/Account.vue")
+		),
+		ProfileSettings: defineAsyncComponent(() =>
+			import("./Tabs/Profile.vue")
+		),
+		PreferencesSettings: defineAsyncComponent(() =>
+			import("./Tabs/Preferences.vue")
+		),
+		RemoveAccount: defineAsyncComponent(() =>
+			import("@/components/modals/RemoveAccount.vue")
+		)
 	},
 	mixins: [TabQueryHandler],
 	data() {

+ 81 - 76
frontend/src/pages/Station/Sidebar/Playlists.vue

@@ -1,89 +1,94 @@
 <template>
 	<div id="my-playlists">
 		<draggable
-			class="menu-list scrollable-list"
+			tag="transition-group"
+			:component-data="{
+				name: !drag ? 'draggable-list-transition' : null
+			}"
 			v-if="playlists.length > 0"
 			v-model="playlists"
+			item-key="_id"
 			v-bind="dragOptions"
 			@start="drag = true"
 			@end="drag = false"
 			@change="savePlaylistOrder"
 		>
-			<transition-group
-				type="transition"
-				:name="!drag ? 'draggable-list-transition' : null"
-			>
-				<playlist-item
-					:playlist="playlist"
-					v-for="playlist in playlists"
-					:key="`key-${playlist._id}`"
-					class="item-draggable"
-				>
-					<div class="icons-group" slot="actions">
-						<i
-							v-if="isExcluded(playlist._id)"
-							class="material-icons stop-icon"
-							content="This playlist is blacklisted in this station"
-							v-tippy="{ theme: 'info' }"
-							>play_disabled</i
-						>
-						<i
-							v-if="
-								station.type === 'community' &&
-									(isOwnerOrAdmin() || station.partyMode) &&
-									!isSelected(playlist._id) &&
-									!isExcluded(playlist._id)
-							"
-							@click="selectPlaylist(playlist)"
-							class="material-icons play-icon"
-							:content="
-								station.partyMode
-									? 'Request songs from this playlist'
-									: 'Play songs from this playlist'
-							"
-							v-tippy
-							>play_arrow</i
-						>
-						<confirm
-							v-if="
-								station.type === 'community' &&
-									(isOwnerOrAdmin() || station.partyMode) &&
-									isSelected(playlist._id)
-							"
-							@confirm="deselectPlaylist(playlist._id)"
-						>
-							<i
-								class="material-icons stop-icon"
-								:content="
-									station.partyMode
-										? 'Stop requesting songs from this playlist'
-										: 'Stop playing songs from this playlist'
-								"
-								v-tippy
-								>stop</i
-							>
-						</confirm>
-						<confirm
-							v-if="isOwnerOrAdmin() && !isExcluded(playlist._id)"
-							@confirm="blacklistPlaylist(playlist._id)"
-						>
-							<i
-								class="material-icons stop-icon"
-								content="Blacklist Playlist"
-								v-tippy
-								>block</i
-							>
-						</confirm>
-						<i
-							@click="edit(playlist._id)"
-							class="material-icons edit-icon"
-							content="Edit Playlist"
-							v-tippy
-							>edit</i
-						>
-					</div>
-				</playlist-item>
-			</transition-group>
+			<template #item="{element}">
+				<div class="menu-list scrollable-list">
+					<playlist-item :playlist="element" class="item-draggable">
+						<template #actions>
+							<div class="icons-group">
+								<i
+									v-if="isExcluded(element._id)"
+									class="material-icons stop-icon"
+									content="This playlist is blacklisted in this station"
+									v-tippy="{ theme: 'info' }"
+									>play_disabled</i
+								>
+								<i
+									v-if="
+										station.type === 'community' &&
+											(isOwnerOrAdmin() ||
+												station.partyMode) &&
+											!isSelected(element._id) &&
+											!isExcluded(element._id)
+									"
+									@click="selectPlaylist(element)"
+									class="material-icons play-icon"
+									:content="
+										station.partyMode
+											? 'Request songs from this playlist'
+											: 'Play songs from this playlist'
+									"
+									v-tippy
+									>play_arrow</i
+								>
+								<confirm
+									v-if="
+										station.type === 'community' &&
+											(isOwnerOrAdmin() ||
+												station.partyMode) &&
+											isSelected(element._id)
+									"
+									@confirm="deselectPlaylist(element._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										:content="
+											station.partyMode
+												? 'Stop requesting songs from this playlist'
+												: 'Stop playing songs from this playlist'
+										"
+										v-tippy
+										>stop</i
+									>
+								</confirm>
+								<confirm
+									v-if="
+										isOwnerOrAdmin() &&
+											!isExcluded(element._id)
+									"
+									@confirm="blacklistPlaylist(element._id)"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Blacklist Playlist"
+										v-tippy
+										>block</i
+									>
+								</confirm>
+								<i
+									@click="edit(element._id)"
+									class="material-icons edit-icon"
+									content="Edit Playlist"
+									v-tippy
+									>edit</i
+								>
+							</div>
+						</template>
+					</playlist-item>
+				</div>
+			</template>
 		</draggable>
 		<p v-else class="nothing-here-text scrollable-list">
 			No Playlists found

+ 1 - 2
frontend/src/pages/Station/Sidebar/index.vue

@@ -41,9 +41,8 @@
 <script>
 import { mapActions, mapState } from "vuex";
 
-import TabQueryHandler from "@/mixins/TabQueryHandler.vue";
-
 import Queue from "@/components/Queue.vue";
+import TabQueryHandler from "@/mixins/TabQueryHandler.vue";
 import Users from "./Users.vue";
 import Playlists from "./Playlists.vue";
 

+ 59 - 43
frontend/src/pages/Station/index.vue

@@ -435,34 +435,38 @@
 										:song="currentSong"
 										placement="top-end"
 									>
-										<div
-											slot="button"
-											id="add-song-to-playlist"
-											content="Add Song to Playlist"
-											v-tippy
-										>
-											<div class="control has-addons">
-												<button
-													class="button is-primary"
-												>
-													<i class="material-icons"
-														>playlist_add</i
+										<template #button>
+											<div
+												id="add-song-to-playlist"
+												content="Add Song to Playlist"
+												v-tippy
+											>
+												<div class="control has-addons">
+													<button
+														class="button is-primary"
 													>
-												</button>
-												<button
-													class="button"
-													id="dropdown-toggle"
-												>
-													<i class="material-icons">
-														{{
-															showPlaylistDropdown
-																? "expand_more"
-																: "expand_less"
-														}}
-													</i>
-												</button>
+														<i
+															class="material-icons"
+															>playlist_add</i
+														>
+													</button>
+													<button
+														class="button"
+														id="dropdown-toggle"
+													>
+														<i
+															class="material-icons"
+														>
+															{{
+																showPlaylistDropdown
+																	? "expand_more"
+																	: "expand_less"
+															}}
+														</i>
+													</button>
+												</div>
 											</div>
-										</div>
+										</template>
 									</add-to-playlist-dropdown>
 								</div>
 								<div id="right-buttons" v-else>
@@ -621,6 +625,7 @@
 
 <script>
 import { mapState, mapActions, mapGetters } from "vuex";
+import { defineAsyncComponent } from "vue";
 import Toast from "toasters";
 import { ContentLoader } from "vue-content-loader";
 
@@ -645,19 +650,31 @@ export default {
 		ContentLoader,
 		MainHeader,
 		MainFooter,
-		RequestSong: () => import("@/components/modals/RequestSong.vue"),
-		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
-		CreatePlaylist: () => import("@/components/modals/CreatePlaylist.vue"),
-		ManageStationOwen: () =>
-			import("@/components/modals/ManageStationOwen/index.vue"),
-		ManageStationKris: () =>
-			import("@/components/modals/ManageStationKris/index.vue"),
-		Report: () => import("@/components/modals/Report.vue"),
+		RequestSong: defineAsyncComponent(() =>
+			import("@/components/modals/RequestSong.vue")
+		),
+		EditPlaylist: defineAsyncComponent(() =>
+			import("@/components/modals/EditPlaylist")
+		),
+		CreatePlaylist: defineAsyncComponent(() =>
+			import("@/components/modals/CreatePlaylist.vue")
+		),
+		ManageStationOwen: defineAsyncComponent(() =>
+			import("@/components/modals/ManageStationOwen/index.vue")
+		),
+		ManageStationKris: defineAsyncComponent(() =>
+			import("@/components/modals/ManageStationKris/index.vue")
+		),
+		Report: defineAsyncComponent(() =>
+			import("@/components/modals/Report.vue")
+		),
 		Z404,
 		FloatingBox,
 		StationSidebar,
 		AddToPlaylistDropdown,
-		EditSong: () => import("@/components/modals/EditSong"),
+		EditSong: defineAsyncComponent(() =>
+			import("@/components/modals/EditSong")
+		),
 		SongItem
 	},
 	data() {
@@ -967,12 +984,10 @@ export default {
 			document.body.style.cssText = `--primary-color: var(--${theme})`;
 		});
 
-		this.socket.on("event:station.name.updated", res => {
+		this.socket.on("event:station.name.updated", async res => {
 			this.station.name = res.data.name;
-			// eslint-disable-next-line no-restricted-globals
-			history.pushState(
-				{},
-				null,
+
+			await this.$router.push(
 				`${res.data.name}?${Object.keys(this.$route.query)
 					.map(key => {
 						return `${encodeURIComponent(key)}=${encodeURIComponent(
@@ -981,6 +996,9 @@ export default {
 					})
 					.join("&")}`
 			);
+
+			// eslint-disable-next-line no-restricted-globals
+			history.replaceState({ ...history.state, ...{} }, null);
 		});
 
 		this.socket.on("event:station.displayName.updated", res => {
@@ -1032,7 +1050,7 @@ export default {
 			this.volumeSliderValue = volume * 100;
 		}
 	},
-	beforeDestroy() {
+	onBeforeUnmount() {
 		document.body.style.cssText = "";
 
 		/** Reset Songslist */
@@ -1212,8 +1230,6 @@ export default {
 				this.updateNoSong(true);
 			}
 
-			console.log(666);
-
 			this.calculateTimeElapsed();
 			this.resizeSeekerbar();
 		},

+ 2 - 5
frontend/src/store/index.js

@@ -1,6 +1,5 @@
 /* eslint-disable import/no-cycle */
-import Vue from "vue";
-import Vuex from "vuex";
+import { createStore } from "vuex";
 
 import websockets from "./modules/websockets";
 
@@ -18,9 +17,7 @@ import viewPunishmentModal from "./modules/modals/viewPunishment";
 import viewReportModal from "./modules/modals/viewReport";
 import reportModal from "./modules/modals/report";
 
-Vue.use(Vuex);
-
-export default new Vuex.Store({
+export default createStore({
 	modules: {
 		websockets,
 		user,

+ 3 - 1
frontend/src/store/modules/admin.js

@@ -1,9 +1,11 @@
 /* eslint no-param-reassign: 0 */
 /* eslint-disable import/no-cycle */
 
-import Vue from "vue";
+// import Vue from "vue";
 import admin from "@/api/admin/index";
 
+const Vue = {};
+
 const state = {};
 const getters = {};
 const actions = {};

+ 0 - 3
frontend/src/store/modules/modals/editPlaylist.js

@@ -1,8 +1,5 @@
 /* eslint no-param-reassign: 0 */
 
-// import Vue from "vue";
-// import admin from "@/api/admin/index";
-
 export default {
 	namespaced: true,
 	state: {

+ 0 - 3
frontend/src/store/modules/modals/editSong.js

@@ -1,8 +1,5 @@
 /* eslint no-param-reassign: 0 */
 
-// import Vue from "vue";
-// import admin from "@/api/admin/index";
-
 export default {
 	namespaced: true,
 	state: {

+ 1 - 1
frontend/webpack.common.js

@@ -55,7 +55,7 @@ module.exports = {
 					{
 						loader: "css-loader",
 						options: {
-							url: false,
+							url: false
 						}
 					},
 					"sass-loader"

+ 2 - 2
frontend/webpack.dev.js

@@ -12,8 +12,8 @@ module.exports = merge(common, {
 	},
 	resolve: {
 		alias: {
-			vue: "vue/dist/vue.js",
-			styles: "src/styles"
+			styles: "src/styles",
+			vue: "vue/dist/vue.esm-bundler.js"
 		}
 	},
 	devServer: {