LeftSidebar.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. <script lang="ts" setup>
  2. import { computed, defineAsyncComponent, reactive, ref } from "vue";
  3. import Toast from "toasters";
  4. import { Station } from "@/types/station";
  5. import { useUserAuthStore } from "@/stores/userAuth";
  6. import { useWebsocketsStore } from "@/stores/websockets";
  7. const props = defineProps<{
  8. station: Station;
  9. canVoteToSkip: boolean;
  10. votedToSkip: boolean;
  11. votesToSkip: number;
  12. votesRequiredToSkip: number;
  13. }>();
  14. const Button = defineAsyncComponent(
  15. () => import("@/pages/NewStation/Components/Button.vue")
  16. );
  17. const Pill = defineAsyncComponent(
  18. () => import("@/pages/NewStation/Components/Pill.vue")
  19. );
  20. const Sidebar = defineAsyncComponent(
  21. () => import("@/pages/NewStation/Components/Sidebar.vue")
  22. );
  23. const Tabs = defineAsyncComponent(
  24. () => import("@/pages/NewStation/Components/Tabs.vue")
  25. );
  26. const UserItem = defineAsyncComponent(
  27. () => import("@/pages/NewStation/UserItem.vue")
  28. );
  29. const { loggedIn, hasPermissionForStation } = useUserAuthStore();
  30. const { socket } = useWebsocketsStore();
  31. const isOwner = userId => props.station.owner === userId;
  32. const djSearch = reactive({
  33. query: "",
  34. searchedQuery: "",
  35. page: 0,
  36. count: 0,
  37. resultsLeft: 0,
  38. pageSize: 0,
  39. results: [],
  40. nextPageResultsCount: 0
  41. });
  42. const userTabs = computed(() => {
  43. const tabs = ["Audience"];
  44. if (hasPermissionForStation(props.station._id, "stations.update"))
  45. tabs.push("DJs");
  46. return tabs;
  47. });
  48. const sortedUsers = computed(() =>
  49. props.station.users && props.station.users.loggedIn
  50. ? props.station.users.loggedIn
  51. .slice()
  52. .sort(
  53. (a, b) =>
  54. Number(isOwner(b._id)) - Number(isOwner(a._id)) ||
  55. Number(!isOwner(a._id)) - Number(!isOwner(b._id))
  56. )
  57. : []
  58. );
  59. const favorite = () => {
  60. socket.dispatch("stations.favoriteStation", props.station._id, res => {
  61. if (res.status === "success") {
  62. new Toast("Successfully favorited station.");
  63. } else new Toast(res.message);
  64. });
  65. };
  66. const unfavorite = () => {
  67. socket.dispatch("stations.unfavoriteStation", props.station._id, res => {
  68. if (res.status === "success") {
  69. new Toast("Successfully unfavorited station.");
  70. } else new Toast(res.message);
  71. });
  72. };
  73. const resetDjsSearch = () => {
  74. djSearch.query = "";
  75. djSearch.searchedQuery = "";
  76. djSearch.page = 0;
  77. djSearch.count = 0;
  78. djSearch.resultsLeft = 0;
  79. djSearch.pageSize = 0;
  80. djSearch.results = [];
  81. djSearch.nextPageResultsCount = 0;
  82. };
  83. const searchForDjs = (page: number) => {
  84. if (djSearch.page >= page || djSearch.searchedQuery !== djSearch.query) {
  85. djSearch.results = [];
  86. djSearch.page = 0;
  87. djSearch.count = 0;
  88. djSearch.resultsLeft = 0;
  89. djSearch.pageSize = 0;
  90. djSearch.nextPageResultsCount = 0;
  91. }
  92. djSearch.searchedQuery = djSearch.query;
  93. socket.dispatch("users.search", djSearch.query, page, res => {
  94. const { data } = res;
  95. if (res.status === "success") {
  96. const { count, pageSize, users } = data;
  97. djSearch.results = [...djSearch.results, ...users];
  98. djSearch.page = page;
  99. djSearch.count = count;
  100. djSearch.resultsLeft = count - djSearch.results.length;
  101. djSearch.pageSize = pageSize;
  102. djSearch.nextPageResultsCount = Math.min(
  103. djSearch.pageSize,
  104. djSearch.resultsLeft
  105. );
  106. } else if (res.status === "error") {
  107. djSearch.results = [];
  108. djSearch.page = 0;
  109. djSearch.count = 0;
  110. djSearch.resultsLeft = 0;
  111. djSearch.pageSize = 0;
  112. djSearch.nextPageResultsCount = 0;
  113. new Toast(res.message);
  114. }
  115. });
  116. };
  117. </script>
  118. <template>
  119. <Sidebar>
  120. <section class="information">
  121. <div class="information__header">
  122. <i
  123. v-if="loggedIn && station.isFavorited"
  124. class="material-icons"
  125. @click.prevent="unfavorite"
  126. title="Favorite station"
  127. >star</i
  128. >
  129. <i
  130. v-else-if="loggedIn"
  131. class="material-icons"
  132. @click.prevent="favorite"
  133. title="Unfavorite station"
  134. >star_border</i
  135. >
  136. <h1>{{ station.displayName }}</h1>
  137. </div>
  138. <div class="information__actions">
  139. <Pill v-if="station.privacy === 'public'" icon="public">
  140. Public
  141. </Pill>
  142. <Pill v-else-if="station.privacy === 'unlisted'" icon="link">
  143. Unlisted
  144. </Pill>
  145. <Pill v-else-if="station.privacy === 'private'" icon="lock">
  146. Private
  147. </Pill>
  148. </div>
  149. <p style="min-height: 60px">{{ station.description }}</p>
  150. <div class="information__actions">
  151. <Button icon="share">Share</Button>
  152. </div>
  153. </section>
  154. <hr class="sidebar__divider" />
  155. <Tabs :tabs="userTabs">
  156. <template #Audience>
  157. <UserItem
  158. v-for="user in sortedUsers"
  159. :key="`audience-${user._id}`"
  160. :station="station"
  161. :user="user"
  162. />
  163. <p
  164. v-if="station.users && station.users.loggedOut?.length > 0"
  165. class="guest-users"
  166. >
  167. {{ sortedUsers.length > 0 ? "..and" : "There are" }}
  168. {{ station.users.loggedOut.length }} logged-out users.
  169. </p>
  170. </template>
  171. <template #DJs>
  172. <h3 style="margin: 0; font-size: 16px; font-weight: 600">
  173. Add DJ
  174. </h3>
  175. <p style="font-size: 14px">
  176. Search for a user to promote to DJ.
  177. </p>
  178. <div style="display: flex">
  179. <input
  180. type="text"
  181. style="
  182. line-height: 18px;
  183. font-size: 12px;
  184. padding: 5px 10px;
  185. background-color: var(--light-grey-2);
  186. border-radius: 5px 0 0 5px;
  187. border: solid 1px var(--light-grey-1);
  188. flex-grow: 1;
  189. border-right-width: 0;
  190. "
  191. v-model="djSearch.query"
  192. @keyup.enter="searchForDjs(1)"
  193. />
  194. <Button
  195. icon="restart_alt"
  196. square
  197. style="
  198. border-right-width: 0;
  199. background-color: var(--light-grey-2);
  200. border-color: var(--light-grey-1);
  201. color: var(--primary-color);
  202. border-radius: 0;
  203. "
  204. @click.prevent="resetDjsSearch()"
  205. title="Reset search"
  206. />
  207. <Button
  208. icon="search"
  209. square
  210. style="border-radius: 0 5px 5px 0; flex-shrink: 0"
  211. @click.prevent="searchForDjs(1)"
  212. title="Search"
  213. />
  214. </div>
  215. <UserItem
  216. v-for="dj in djSearch.results"
  217. :key="`dj-search-${dj._id}`"
  218. :station="station"
  219. :user="dj"
  220. />
  221. <Button
  222. v-if="djSearch.resultsLeft > 0"
  223. icon="search"
  224. @click.prevent="searchForDjs(djSearch.page + 1)"
  225. >
  226. Load {{ djSearch.nextPageResultsCount }}
  227. more results
  228. </Button>
  229. <p
  230. v-if="djSearch.page > 0 && djSearch.results.length === 0"
  231. class="guest-users"
  232. >
  233. No users found with this search query.
  234. </p>
  235. <hr class="sidebar__divider" />
  236. <h3 style="margin: 0; font-size: 16px; font-weight: 600">
  237. Current DJs
  238. </h3>
  239. <UserItem
  240. v-for="dj in station.djs"
  241. :key="`djs-${dj._id}`"
  242. :station="station"
  243. :user="dj"
  244. />
  245. <p v-if="station.djs.length === 0" class="guest-users">
  246. There are currently no DJs.
  247. </p>
  248. </template>
  249. </Tabs>
  250. </Sidebar>
  251. </template>
  252. <style lang="less" scoped>
  253. .information {
  254. display: flex;
  255. flex-direction: column;
  256. gap: 10px;
  257. &__header {
  258. display: flex;
  259. align-items: center;
  260. gap: 10px;
  261. h1 {
  262. font-size: 26px;
  263. font-weight: 600;
  264. margin: 0;
  265. }
  266. i {
  267. font-size: 26px;
  268. color: var(--yellow);
  269. // TODO: Wrap in button
  270. }
  271. }
  272. &__actions {
  273. display: flex;
  274. flex-wrap: wrap;
  275. gap: 10px;
  276. }
  277. }
  278. .guest-users {
  279. color: var(--dark-grey-1);
  280. font-size: 14px !important;
  281. font-weight: 500 !important;
  282. text-align: center;
  283. padding: 5px;
  284. }
  285. </style>