Stations.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <template>
  2. <div>
  3. <metadata title="Admin | Stations" />
  4. <div class="container">
  5. <button class="button is-primary" @click="clearEveryStationQueue()">
  6. Clear every station queue
  7. </button>
  8. <br />
  9. <br />
  10. <table class="table is-striped">
  11. <thead>
  12. <tr>
  13. <td>ID</td>
  14. <td>Name</td>
  15. <td>Type</td>
  16. <td>Display Name</td>
  17. <td>Description</td>
  18. <td>Owner</td>
  19. <td>Options</td>
  20. </tr>
  21. </thead>
  22. <tbody>
  23. <tr v-for="(station, index) in stations" :key="index">
  24. <td>
  25. <span>{{ station._id }}</span>
  26. </td>
  27. <td>
  28. <span>
  29. <router-link
  30. :to="{
  31. name: 'station',
  32. params: { id: station.name }
  33. }"
  34. >
  35. {{ station.name }}
  36. </router-link>
  37. </span>
  38. </td>
  39. <td>
  40. <span>{{ station.type }}</span>
  41. </td>
  42. <td>
  43. <span>{{ station.displayName }}</span>
  44. </td>
  45. <td>
  46. <span>{{ station.description }}</span>
  47. </td>
  48. <td>
  49. <span
  50. v-if="station.type === 'official'"
  51. title="Musare"
  52. >Musare</span
  53. >
  54. <user-id-to-username
  55. v-else
  56. :user-id="station.owner"
  57. :link="true"
  58. />
  59. </td>
  60. <td>
  61. <a class="button is-info" @click="edit(station)"
  62. >Edit</a
  63. >
  64. <a
  65. class="button is-danger"
  66. href="#"
  67. @click="removeStation(index)"
  68. >Remove</a
  69. >
  70. </td>
  71. </tr>
  72. </tbody>
  73. </table>
  74. </div>
  75. <div class="container">
  76. <div class="card is-fullwidth">
  77. <header class="card-header">
  78. <p class="card-header-title">Create official station</p>
  79. </header>
  80. <div class="card-content">
  81. <div class="content">
  82. <div class="control is-horizontal">
  83. <div class="control is-grouped">
  84. <p class="control is-expanded">
  85. <input
  86. v-model="newStation.name"
  87. class="input"
  88. type="text"
  89. placeholder="Name"
  90. />
  91. </p>
  92. <p class="control is-expanded">
  93. <input
  94. v-model="newStation.displayName"
  95. class="input"
  96. type="text"
  97. placeholder="Display Name"
  98. />
  99. </p>
  100. </div>
  101. </div>
  102. <label class="label">Description</label>
  103. <p class="control is-expanded">
  104. <input
  105. v-model="newStation.description"
  106. class="input"
  107. type="text"
  108. placeholder="Short description"
  109. />
  110. </p>
  111. <div class="control is-grouped genre-wrapper">
  112. <div class="sector">
  113. <p class="control has-addons">
  114. <input
  115. id="new-genre"
  116. class="input"
  117. type="text"
  118. placeholder="Genre"
  119. @keyup.enter="addGenre()"
  120. />
  121. <a
  122. class="button is-info"
  123. href="#"
  124. @click="addGenre()"
  125. >Add genre</a
  126. >
  127. </p>
  128. <span
  129. v-for="(genre, index) in newStation.genres"
  130. :key="index"
  131. class="tag is-info"
  132. >
  133. {{ genre }}
  134. <button
  135. class="delete is-info"
  136. @click="removeGenre(index)"
  137. />
  138. </span>
  139. </div>
  140. <div class="sector">
  141. <p class="control has-addons">
  142. <input
  143. id="new-blacklisted-genre"
  144. class="input"
  145. type="text"
  146. placeholder="Blacklisted Genre"
  147. @keyup.enter="addBlacklistedGenre()"
  148. />
  149. <a
  150. class="button is-info"
  151. href="#"
  152. @click="addBlacklistedGenre()"
  153. >Add blacklisted genre</a
  154. >
  155. </p>
  156. <span
  157. v-for="(genre,
  158. index) in newStation.blacklistedGenres"
  159. :key="index"
  160. class="tag is-info"
  161. >
  162. {{ genre }}
  163. <button
  164. class="delete is-info"
  165. @click="removeBlacklistedGenre(index)"
  166. />
  167. </span>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. <footer class="card-footer">
  173. <a
  174. class="card-footer-item"
  175. href="#"
  176. @click="createStation()"
  177. >Create</a
  178. >
  179. </footer>
  180. </div>
  181. </div>
  182. <edit-station
  183. v-if="modals.editStation"
  184. :station-id="editingStationId"
  185. sector="admin"
  186. />
  187. </div>
  188. </template>
  189. <script>
  190. import { mapState, mapActions, mapGetters } from "vuex";
  191. import Toast from "toasters";
  192. import ws from "../../../ws";
  193. import UserIdToUsername from "../../../components/common/UserIdToUsername.vue";
  194. export default {
  195. components: {
  196. EditStation: () => import("../../../components/modals/EditStation.vue"),
  197. UserIdToUsername
  198. },
  199. data() {
  200. return {
  201. editingStationId: "",
  202. newStation: {
  203. genres: [],
  204. blacklistedGenres: []
  205. }
  206. };
  207. },
  208. computed: {
  209. ...mapState("admin/stations", {
  210. stations: state => state.stations
  211. }),
  212. ...mapState("modalVisibility", {
  213. modals: state => state.modals.admin
  214. }),
  215. ...mapGetters({
  216. socket: "websockets/getSocket"
  217. })
  218. },
  219. mounted() {
  220. if (this.socket.readyState === 1) this.init();
  221. ws.onConnect(() => this.init());
  222. this.socket.on("event:admin.station.added", station =>
  223. this.stationAdded(station)
  224. );
  225. this.socket.on("event:admin.station.removed", stationId =>
  226. this.stationRemoved(stationId)
  227. );
  228. },
  229. methods: {
  230. createStation() {
  231. const {
  232. newStation: {
  233. name,
  234. displayName,
  235. description,
  236. genres,
  237. blacklistedGenres
  238. }
  239. } = this;
  240. if (name === undefined)
  241. return new Toast({
  242. content: "Field (Name) cannot be empty",
  243. timeout: 3000
  244. });
  245. if (displayName === undefined)
  246. return new Toast({
  247. content: "Field (Display Name) cannot be empty",
  248. timeout: 3000
  249. });
  250. if (description === undefined)
  251. return new Toast({
  252. content: "Field (Description) cannot be empty",
  253. timeout: 3000
  254. });
  255. return this.socket.dispatch(
  256. "stations.create",
  257. {
  258. name,
  259. type: "official",
  260. displayName,
  261. description,
  262. genres,
  263. blacklistedGenres
  264. },
  265. result => {
  266. new Toast({ content: result.message, timeout: 3000 });
  267. if (result.status === "success")
  268. this.newStation = {
  269. genres: [],
  270. blacklistedGenres: []
  271. };
  272. }
  273. );
  274. },
  275. removeStation(index) {
  276. this.socket.dispatch(
  277. "stations.remove",
  278. this.stations[index]._id,
  279. res => {
  280. new Toast({ content: res.message, timeout: 3000 });
  281. }
  282. );
  283. },
  284. edit(station) {
  285. this.editingStationId = station._id;
  286. this.openModal({
  287. sector: "admin",
  288. modal: "editStation"
  289. });
  290. },
  291. addGenre() {
  292. const genre = document
  293. .getElementById(`new-genre`)
  294. .value.toLowerCase()
  295. .trim();
  296. if (this.newStation.genres.indexOf(genre) !== -1)
  297. return new Toast({
  298. content: "Genre already exists",
  299. timeout: 3000
  300. });
  301. if (genre) {
  302. this.newStation.genres.push(genre);
  303. document.getElementById(`new-genre`).value = "";
  304. return true;
  305. }
  306. return new Toast({
  307. content: "Genre cannot be empty",
  308. timeout: 3000
  309. });
  310. },
  311. removeGenre(index) {
  312. this.newStation.genres.splice(index, 1);
  313. },
  314. addBlacklistedGenre() {
  315. const genre = document
  316. .getElementById(`new-blacklisted-genre`)
  317. .value.toLowerCase()
  318. .trim();
  319. if (this.newStation.blacklistedGenres.indexOf(genre) !== -1)
  320. return new Toast({
  321. content: "Genre already exists",
  322. timeout: 3000
  323. });
  324. if (genre) {
  325. this.newStation.blacklistedGenres.push(genre);
  326. document.getElementById(`new-blacklisted-genre`).value = "";
  327. return true;
  328. }
  329. return new Toast({
  330. content: "Genre cannot be empty",
  331. timeout: 3000
  332. });
  333. },
  334. removeBlacklistedGenre(index) {
  335. this.newStation.blacklistedGenres.splice(index, 1);
  336. },
  337. clearEveryStationQueue() {
  338. this.socket.dispatch("stations.clearEveryStationQueue", res => {
  339. if (res.status === "success") {
  340. new Toast({
  341. content: `${res.message}`,
  342. timeout: 4000
  343. });
  344. } else {
  345. new Toast({
  346. content: `Error: ${res.message}`,
  347. timeout: 8000
  348. });
  349. }
  350. });
  351. },
  352. init() {
  353. this.socket.dispatch("stations.index", data => {
  354. this.loadStations(data.stations);
  355. });
  356. this.socket.dispatch("apis.joinAdminRoom", "stations", () => {});
  357. },
  358. ...mapActions("modalVisibility", ["openModal"]),
  359. ...mapActions("admin/stations", [
  360. "editStation",
  361. "loadStations",
  362. "stationRemoved",
  363. "stationAdded"
  364. ])
  365. }
  366. };
  367. </script>
  368. <style lang="scss" scoped>
  369. .night-mode {
  370. .table {
  371. color: var(--light-grey-2);
  372. background-color: var(--dark-grey-3);
  373. thead tr {
  374. background: var(--dark-grey-3);
  375. td {
  376. color: var(--white);
  377. }
  378. }
  379. tbody tr:hover {
  380. background-color: var(--dark-grey-4) !important;
  381. }
  382. tbody tr:nth-child(even) {
  383. background-color: var(--dark-grey-2);
  384. }
  385. strong {
  386. color: var(--light-grey-2);
  387. }
  388. }
  389. .card {
  390. background: var(--dark-grey-3);
  391. .card-header {
  392. box-shadow: 0 1px 2px rgba(10, 10, 10, 0.8);
  393. }
  394. p,
  395. .label {
  396. color: var(--light-grey-2);
  397. }
  398. }
  399. }
  400. .tag {
  401. margin-top: 5px;
  402. &:not(:last-child) {
  403. margin-right: 5px;
  404. }
  405. }
  406. td {
  407. word-wrap: break-word;
  408. max-width: 10vw;
  409. vertical-align: middle;
  410. }
  411. .is-info:focus {
  412. background-color: var(--primary-color);
  413. }
  414. .genre-wrapper {
  415. display: flex;
  416. justify-content: space-around;
  417. }
  418. .card-footer-item {
  419. color: var(--primary-color);
  420. }
  421. </style>