index.vue 14 KB

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