index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. <template>
  2. <div class="app">
  3. <div class="admin-area">
  4. <main-header :class="{ 'admin-sidebar-active': sidebarActive }" />
  5. <div class="admin-content">
  6. <div
  7. class="admin-sidebar"
  8. :class="{ minimised: !sidebarActive }"
  9. >
  10. <div class="inner">
  11. <div
  12. class="bottom"
  13. :style="`padding-bottom: ${sidebarPadding}px`"
  14. >
  15. <div
  16. class="sidebar-item toggle-sidebar"
  17. @click="toggleSidebar()"
  18. content="Expand"
  19. v-tippy="{ onShow: () => !sidebarActive }"
  20. >
  21. <i class="material-icons">menu_open</i>
  22. <span>Minimise</span>
  23. </div>
  24. <div
  25. v-if="sidebarActive"
  26. class="sidebar-item with-children"
  27. :class="{ 'is-active': childrenActive.songs }"
  28. >
  29. <span>
  30. <router-link to="/admin/songs">
  31. <i class="material-icons">music_note</i>
  32. <span>Songs</span>
  33. </router-link>
  34. <i
  35. class="material-icons toggle-sidebar-children"
  36. @click="
  37. toggleChildren({ child: 'songs' })
  38. "
  39. >
  40. {{
  41. childrenActive.songs
  42. ? "expand_less"
  43. : "expand_more"
  44. }}
  45. </i>
  46. </span>
  47. <div class="sidebar-item-children">
  48. <router-link
  49. class="sidebar-item-child"
  50. to="/admin/songs"
  51. >
  52. Songs
  53. </router-link>
  54. <router-link
  55. class="sidebar-item-child"
  56. to="/admin/songs/import"
  57. >
  58. Import
  59. </router-link>
  60. </div>
  61. </div>
  62. <router-link
  63. v-else
  64. class="sidebar-item songs"
  65. to="/admin/songs"
  66. content="Songs"
  67. v-tippy="{
  68. theme: 'info',
  69. onShow: () => !sidebarActive
  70. }"
  71. >
  72. <i class="material-icons">music_note</i>
  73. <span>Songs</span>
  74. </router-link>
  75. <router-link
  76. class="sidebar-item reports"
  77. to="/admin/reports"
  78. content="Reports"
  79. v-tippy="{
  80. theme: 'info',
  81. onShow: () => !sidebarActive
  82. }"
  83. >
  84. <i class="material-icons">flag</i>
  85. <span>Reports</span>
  86. </router-link>
  87. <router-link
  88. class="sidebar-item stations"
  89. to="/admin/stations"
  90. content="Stations"
  91. v-tippy="{
  92. theme: 'info',
  93. onShow: () => !sidebarActive
  94. }"
  95. >
  96. <i class="material-icons">radio</i>
  97. <span>Stations</span>
  98. </router-link>
  99. <router-link
  100. class="sidebar-item playlists"
  101. to="/admin/playlists"
  102. content="Playlists"
  103. v-tippy="{
  104. theme: 'info',
  105. onShow: () => !sidebarActive
  106. }"
  107. >
  108. <i class="material-icons">library_music</i>
  109. <span>Playlists</span>
  110. </router-link>
  111. <div
  112. v-if="sidebarActive"
  113. class="sidebar-item with-children"
  114. :class="{ 'is-active': childrenActive.users }"
  115. >
  116. <span>
  117. <router-link to="/admin/users">
  118. <i class="material-icons">people</i>
  119. <span>Users</span>
  120. </router-link>
  121. <i
  122. class="material-icons toggle-sidebar-children"
  123. @click="
  124. toggleChildren({ child: 'users' })
  125. "
  126. >
  127. {{
  128. childrenActive.users
  129. ? "expand_less"
  130. : "expand_more"
  131. }}
  132. </i>
  133. </span>
  134. <div class="sidebar-item-children">
  135. <router-link
  136. class="sidebar-item-child"
  137. to="/admin/users"
  138. >
  139. Users
  140. </router-link>
  141. <router-link
  142. class="sidebar-item-child"
  143. to="/admin/users/data-requests"
  144. >
  145. Data Requests
  146. </router-link>
  147. <router-link
  148. class="sidebar-item-child"
  149. to="/admin/users/punishments"
  150. >
  151. Punishments
  152. </router-link>
  153. </div>
  154. </div>
  155. <router-link
  156. v-else
  157. class="sidebar-item users"
  158. to="/admin/users"
  159. content="Users"
  160. v-tippy="{
  161. theme: 'info',
  162. onShow: () => !sidebarActive
  163. }"
  164. >
  165. <i class="material-icons">people</i>
  166. <span>Users</span>
  167. </router-link>
  168. <router-link
  169. class="sidebar-item news"
  170. to="/admin/news"
  171. content="News"
  172. v-tippy="{
  173. theme: 'info',
  174. onShow: () => !sidebarActive
  175. }"
  176. >
  177. <i class="material-icons">chrome_reader_mode</i>
  178. <span>News</span>
  179. </router-link>
  180. <router-link
  181. class="sidebar-item statistics"
  182. to="/admin/statistics"
  183. content="Statistics"
  184. v-tippy="{
  185. theme: 'info',
  186. onShow: () => !sidebarActive
  187. }"
  188. >
  189. <i class="material-icons">show_chart</i>
  190. <span>Statistics</span>
  191. </router-link>
  192. <div
  193. v-if="sidebarActive"
  194. class="sidebar-item with-children"
  195. :class="{ 'is-active': childrenActive.youtube }"
  196. >
  197. <span>
  198. <router-link to="/admin/youtube">
  199. <i class="material-icons"
  200. >smart_display</i
  201. >
  202. <span>YouTube</span>
  203. </router-link>
  204. <i
  205. class="material-icons toggle-sidebar-children"
  206. @click="
  207. toggleChildren({ child: 'youtube' })
  208. "
  209. >
  210. {{
  211. childrenActive.youtube
  212. ? "expand_less"
  213. : "expand_more"
  214. }}
  215. </i>
  216. </span>
  217. <div class="sidebar-item-children">
  218. <router-link
  219. class="sidebar-item-child"
  220. to="/admin/youtube"
  221. >
  222. YouTube
  223. </router-link>
  224. <router-link
  225. class="sidebar-item-child"
  226. to="/admin/youtube/videos"
  227. >
  228. Videos
  229. </router-link>
  230. </div>
  231. </div>
  232. <router-link
  233. v-else
  234. class="sidebar-item youtube"
  235. to="/admin/youtube"
  236. content="YouTube"
  237. v-tippy="{
  238. theme: 'info',
  239. onShow: () => !sidebarActive
  240. }"
  241. >
  242. <i class="material-icons">smart_display</i>
  243. <span>YouTube</span>
  244. </router-link>
  245. </div>
  246. </div>
  247. </div>
  248. <div class="admin-container">
  249. <div class="admin-tab-container">
  250. <router-view></router-view>
  251. </div>
  252. <main-footer />
  253. </div>
  254. </div>
  255. </div>
  256. <floating-box
  257. id="keyboardShortcutsHelper"
  258. ref="keyboardShortcutsHelper"
  259. >
  260. <template #body>
  261. <div>
  262. <div>
  263. <span class="biggest"
  264. ><b>Keyboard shortcuts helper</b></span
  265. >
  266. <span
  267. ><b>Ctrl + /</b> - Toggles this keyboard shortcuts
  268. helper</span
  269. >
  270. <span
  271. ><b>Ctrl + Shift + /</b> - Resets the position of
  272. this keyboard shortcuts helper</span
  273. >
  274. <hr />
  275. </div>
  276. <div>
  277. <span class="biggest"><b>Table</b></span>
  278. <span class="bigger"><b>Navigation</b></span>
  279. <span
  280. ><b>Up / Down arrow keys</b> - Move between
  281. rows</span
  282. >
  283. <hr />
  284. </div>
  285. <div>
  286. <span class="bigger"><b>Page navigation</b></span>
  287. <span
  288. ><b>Ctrl + Left/Right arrow keys</b> - Previous/next
  289. page</span
  290. >
  291. <span
  292. ><b>Ctrl + Shift + Left/Right arrow keys</b> -
  293. First/last page</span
  294. >
  295. <hr />
  296. </div>
  297. <div>
  298. <span class="bigger"><b>Reset localStorage</b></span>
  299. <span><b>Ctrl + F5</b> - Resets localStorage</span>
  300. <hr />
  301. </div>
  302. <div>
  303. <span class="bigger"><b>Selecting</b></span>
  304. <span><b>Space</b> - Selects/unselects a row</span>
  305. <span><b>Ctrl + A</b> - Selects all rows</span>
  306. <span
  307. ><b>Shift + Up/Down arrow keys</b> - Selects all
  308. rows in between</span
  309. >
  310. <span
  311. ><b>Ctrl + Up/Down arrow keys</b> - Unselects all
  312. rows in between</span
  313. >
  314. <hr />
  315. </div>
  316. <div>
  317. <span class="bigger"><b>Popup actions</b></span>
  318. <span><b>Ctrl + 1-9</b> - Execute action 1-9</span>
  319. <span><b>Ctrl + 0</b> - Select action 1</span>
  320. <hr />
  321. </div>
  322. </div>
  323. </template>
  324. </floating-box>
  325. </div>
  326. </template>
  327. <script>
  328. import { mapState, mapActions, mapGetters } from "vuex";
  329. import keyboardShortcuts from "@/keyboardShortcuts";
  330. import FloatingBox from "@/components/FloatingBox.vue";
  331. export default {
  332. components: {
  333. FloatingBox
  334. },
  335. data() {
  336. return {
  337. currentTab: "",
  338. siteSettings: {
  339. logo: "",
  340. sitename: ""
  341. },
  342. sidebarActive: true,
  343. sidebarPadding: 0
  344. };
  345. },
  346. computed: {
  347. ...mapGetters({
  348. socket: "websockets/getSocket"
  349. }),
  350. ...mapState("admin", { childrenActive: state => state.childrenActive })
  351. },
  352. watch: {
  353. $route(route) {
  354. if (this.getTabFromPath(route.path)) this.onRouteChange();
  355. }
  356. },
  357. async mounted() {
  358. if (this.getTabFromPath()) {
  359. this.onRouteChange();
  360. } else if (localStorage.getItem("lastAdminPage")) {
  361. this.$router.push(
  362. `/admin/${localStorage.getItem("lastAdminPage")}`
  363. );
  364. } else {
  365. this.$router.push(`/admin/songs`);
  366. }
  367. this.siteSettings = await lofig.get("siteSettings");
  368. this.sidebarActive = JSON.parse(
  369. localStorage.getItem("admin-sidebar-active")
  370. );
  371. if (this.sidebarActive === null)
  372. this.sidebarActive = !(document.body.clientWidth <= 768);
  373. this.calculateSidebarPadding();
  374. window.addEventListener("scroll", this.calculateSidebarPadding);
  375. keyboardShortcuts.registerShortcut(
  376. "admin.toggleKeyboardShortcutsHelper",
  377. {
  378. keyCode: 191, // '/' key
  379. ctrl: true,
  380. preventDefault: true,
  381. handler: () => {
  382. this.toggleKeyboardShortcutsHelper();
  383. }
  384. }
  385. );
  386. keyboardShortcuts.registerShortcut(
  387. "admin.resetKeyboardShortcutsHelper",
  388. {
  389. keyCode: 191, // '/' key
  390. ctrl: true,
  391. shift: true,
  392. preventDefault: true,
  393. handler: () => {
  394. this.resetKeyboardShortcutsHelper();
  395. }
  396. }
  397. );
  398. },
  399. beforeUnmount() {
  400. this.socket.dispatch("apis.leaveRooms");
  401. window.removeEventListener("scroll", this.calculateSidebarPadding);
  402. const shortcutNames = [
  403. "admin.toggleKeyboardShortcutsHelper",
  404. "admin.resetKeyboardShortcutsHelper"
  405. ];
  406. shortcutNames.forEach(shortcutName => {
  407. keyboardShortcuts.unregisterShortcut(shortcutName);
  408. });
  409. },
  410. methods: {
  411. onRouteChange() {
  412. if (this.currentTab.startsWith("songs")) {
  413. this.toggleChildren({ child: "songs", force: false });
  414. } else if (this.currentTab.startsWith("users")) {
  415. this.toggleChildren({ child: "users", force: false });
  416. } else if (this.currentTab.startsWith("youtube")) {
  417. this.toggleChildren({ child: "youtube", force: false });
  418. }
  419. this.currentTab = this.getTabFromPath();
  420. if (this.$refs[`${this.currentTab}-tab`])
  421. this.$refs[`${this.currentTab}-tab`].scrollIntoView({
  422. inline: "center",
  423. block: "nearest"
  424. });
  425. localStorage.setItem("lastAdminPage", this.currentTab);
  426. if (this.currentTab.startsWith("songs"))
  427. this.toggleChildren({ child: "songs", force: true });
  428. else if (this.currentTab.startsWith("users"))
  429. this.toggleChildren({ child: "users", force: true });
  430. else if (this.currentTab.startsWith("youtube"))
  431. this.toggleChildren({ child: "youtube", force: true });
  432. },
  433. toggleKeyboardShortcutsHelper() {
  434. this.$refs.keyboardShortcutsHelper.toggleBox();
  435. },
  436. resetKeyboardShortcutsHelper() {
  437. this.$refs.keyboardShortcutsHelper.resetBox();
  438. },
  439. toggleSidebar() {
  440. this.sidebarActive = !this.sidebarActive;
  441. localStorage.setItem("admin-sidebar-active", this.sidebarActive);
  442. },
  443. getTabFromPath(path) {
  444. const localPath = path || this.$route.path;
  445. return localPath.substr(0, 7) === "/admin/"
  446. ? localPath.substr(7, localPath.length)
  447. : null;
  448. },
  449. calculateSidebarPadding() {
  450. const scrollTop =
  451. document.documentElement.scrollTop || document.scrollTop || 0;
  452. if (scrollTop <= 64) this.sidebarPadding = 64 - scrollTop;
  453. else this.sidebarPadding = 0;
  454. },
  455. ...mapActions("admin", ["toggleChildren"])
  456. }
  457. };
  458. </script>
  459. <style lang="less" scoped>
  460. .night-mode {
  461. .main-container .admin-area {
  462. .admin-sidebar .inner {
  463. .top {
  464. background-color: var(--dark-grey-3);
  465. }
  466. .bottom {
  467. background-color: var(--dark-grey-2);
  468. .sidebar-item {
  469. background-color: var(--dark-grey-2);
  470. border-color: var(--dark-grey-3);
  471. &,
  472. &.with-children .sidebar-item-child,
  473. &.with-children > span > a {
  474. color: var(--white);
  475. }
  476. }
  477. }
  478. }
  479. :deep(.admin-content .admin-container .admin-tab-container) {
  480. .admin-tab {
  481. .card {
  482. background-color: var(--dark-grey-3);
  483. p {
  484. color: var(--light-grey-2);
  485. }
  486. }
  487. }
  488. }
  489. }
  490. }
  491. .main-container {
  492. height: auto;
  493. .admin-area {
  494. display: flex;
  495. flex-direction: column;
  496. min-height: 100vh;
  497. :deep(.nav) {
  498. .nav-menu.is-active {
  499. left: 45px;
  500. }
  501. &.admin-sidebar-active .nav-menu.is-active {
  502. left: 200px;
  503. }
  504. }
  505. .admin-sidebar {
  506. display: flex;
  507. min-width: 200px;
  508. width: 200px;
  509. @media screen and (max-width: 768px) {
  510. min-width: 45px;
  511. width: 45px;
  512. }
  513. .inner {
  514. display: flex;
  515. flex-direction: column;
  516. max-height: 100vh;
  517. width: 200px;
  518. position: sticky;
  519. top: 0;
  520. bottom: 0;
  521. left: 0;
  522. z-index: 5;
  523. box-shadow: @box-shadow;
  524. .bottom {
  525. overflow-y: auto;
  526. height: 100%;
  527. max-height: 100%;
  528. display: flex;
  529. flex-direction: column;
  530. flex: 1 0 auto;
  531. background-color: var(--white);
  532. .sidebar-item {
  533. display: flex;
  534. padding: 0 20px;
  535. line-height: 40px;
  536. font-size: 16px;
  537. font-weight: 600;
  538. color: var(--primary-color);
  539. background-color: var(--white);
  540. border-bottom: 1px solid var(--light-grey-2);
  541. transition: filter 0.2s ease-in-out;
  542. & > .material-icons {
  543. line-height: 40px;
  544. margin-right: 5px;
  545. }
  546. &:hover,
  547. &:focus,
  548. &.router-link-active,
  549. &.is-active {
  550. filter: brightness(95%);
  551. }
  552. &.toggle-sidebar {
  553. cursor: pointer;
  554. font-weight: 400;
  555. }
  556. &.with-children {
  557. flex-direction: column;
  558. & > span {
  559. display: flex;
  560. line-height: 40px;
  561. cursor: pointer;
  562. & > a {
  563. display: flex;
  564. }
  565. & > .material-icons,
  566. & > a > .material-icons {
  567. line-height: 40px;
  568. margin-right: 5px;
  569. }
  570. }
  571. .toggle-sidebar-children {
  572. margin-left: auto;
  573. }
  574. .sidebar-item-children {
  575. display: none;
  576. }
  577. &.is-active .sidebar-item-children {
  578. display: flex;
  579. flex-direction: column;
  580. .sidebar-item-child {
  581. display: flex;
  582. flex-direction: column;
  583. margin-left: 30px;
  584. font-size: 14px;
  585. line-height: 30px;
  586. position: relative;
  587. &::before {
  588. content: "";
  589. position: absolute;
  590. width: 1px;
  591. height: 30px;
  592. top: 0;
  593. left: -20px;
  594. background-color: var(--light-grey-3);
  595. }
  596. &:last-child::before {
  597. height: 16px;
  598. }
  599. &::after {
  600. content: "";
  601. position: absolute;
  602. width: 15px;
  603. height: 1px;
  604. top: 15px;
  605. left: -20px;
  606. background-color: var(--light-grey-3);
  607. }
  608. &.router-link-active {
  609. filter: brightness(95%);
  610. }
  611. }
  612. }
  613. }
  614. }
  615. }
  616. }
  617. &.minimised {
  618. min-width: 45px;
  619. width: 45px;
  620. .inner {
  621. max-width: 45px;
  622. .top {
  623. justify-content: center;
  624. .full-logo {
  625. display: none;
  626. }
  627. .minimised-logo {
  628. display: flex;
  629. }
  630. }
  631. .sidebar-item {
  632. justify-content: center;
  633. padding: 0;
  634. & > span {
  635. display: none;
  636. }
  637. }
  638. }
  639. }
  640. }
  641. .admin-content {
  642. display: flex;
  643. flex-direction: row;
  644. flex-grow: 1;
  645. .admin-container {
  646. display: flex;
  647. flex-direction: column;
  648. flex-grow: 1;
  649. overflow: hidden;
  650. :deep(.admin-tab-container) {
  651. display: flex;
  652. flex-direction: column;
  653. flex: 1 0 auto;
  654. padding: 10px 10px 20px 10px;
  655. .admin-tab {
  656. display: flex;
  657. flex-direction: column;
  658. width: 100%;
  659. max-width: 1900px;
  660. margin: 0 auto;
  661. padding: 0 10px;
  662. .card {
  663. display: flex;
  664. flex-grow: 1;
  665. flex-direction: column;
  666. padding: 20px;
  667. margin: 10px 0;
  668. border-radius: @border-radius;
  669. background-color: var(--white);
  670. color: var(--dark-grey);
  671. box-shadow: @box-shadow;
  672. h1 {
  673. font-size: 36px;
  674. margin: 0 0 5px 0;
  675. }
  676. h4 {
  677. font-size: 22px;
  678. margin: 0;
  679. }
  680. h5 {
  681. font-size: 18px;
  682. margin: 0;
  683. }
  684. hr {
  685. margin: 10px 0;
  686. }
  687. &.tab-info {
  688. flex-direction: row;
  689. flex-wrap: wrap;
  690. .info-row {
  691. display: flex;
  692. flex-grow: 1;
  693. flex-direction: column;
  694. }
  695. .button-row {
  696. display: flex;
  697. flex-direction: row;
  698. flex-wrap: wrap;
  699. justify-content: center;
  700. margin: auto 0;
  701. padding: 5px 0;
  702. & > .button,
  703. & > span {
  704. margin: auto 0;
  705. &:not(:first-child) {
  706. margin-left: 5px;
  707. }
  708. }
  709. & > span > .control.has-addons {
  710. margin-bottom: 0 !important;
  711. }
  712. }
  713. }
  714. }
  715. @media screen and (min-width: 980px) {
  716. &.container {
  717. margin: 0 auto;
  718. max-width: 960px;
  719. }
  720. }
  721. @media screen and (min-width: 1180px) {
  722. &.container {
  723. max-width: 1200px;
  724. }
  725. }
  726. }
  727. }
  728. }
  729. }
  730. }
  731. }
  732. :deep(.box) {
  733. box-shadow: @box-shadow;
  734. display: block;
  735. &:not(:last-child) {
  736. margin-bottom: 20px;
  737. }
  738. }
  739. #keyboardShortcutsHelper {
  740. .box-body {
  741. .biggest {
  742. font-size: 18px;
  743. }
  744. .bigger {
  745. font-size: 16px;
  746. }
  747. span {
  748. display: block;
  749. }
  750. }
  751. }
  752. </style>