App.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <template>
  2. <div id='app'>
  3. <modal-window v-model='showAjaxErrorsModal' style='z-index: 100' width='25rem' :no-padding='true'>
  4. <div slot='main'>
  5. <p v-for='error in this.$store.state.ajaxErrors' style='margin: 1rem;'>{{error}}</p>
  6. </div>
  7. <button
  8. slot='footer'
  9. class='button button--modal'
  10. @click='showAjaxErrorsModal = false'
  11. ref='ajaxErrorsModalButton'
  12. >
  13. OK
  14. </button>
  15. </modal-window>
  16. <modal-window
  17. v-model='showAccountModal'
  18. @input='closeAccountModal'
  19. :no-padding='true'
  20. :hide-footer='true'
  21. >
  22. <tab-view :tabs='["Sign up", "Login"]' v-model="showAccountTab" padding='true' slot='main'>
  23. <template slot='Sign up'>
  24. <p style='margin-top: 0;' v-if='$store.state.token'>
  25. <strong>Providing the token is still valid, this will create an admin account</strong>
  26. </p>
  27. <p style='margin-top: 0;' v-else>
  28. Sign up to create and post in threads.
  29. <br/>It only takes a few seconds
  30. </p>
  31. <form @submit.prevent='createAccount'>
  32. <fancy-input
  33. v-model='signup.username'
  34. :error='signup.errors.username'
  35. placeholder='Username'
  36. width='100%'
  37. >
  38. </fancy-input>
  39. <fancy-input
  40. v-model='signup.password'
  41. :error='signup.errors.hash'
  42. placeholder='Password'
  43. type='password'
  44. width='100%'
  45. >
  46. </fancy-input>
  47. <fancy-input
  48. v-model='signup.confirmPassword'
  49. :error='signup.errors.confirmPassword'
  50. placeholder='Confirm password'
  51. type='password'
  52. width='100%'
  53. >
  54. </fancy-input>
  55. <div style='margin-top: 0.5rem;'>
  56. <loading-button
  57. class='button--green button--margin'
  58. :loading='signup.loading'
  59. @click='createAccount'
  60. >
  61. Sign up
  62. </loading-button>
  63. <div class='button button--borderless' @click='closeAccountModal'>
  64. Cancel
  65. </div>
  66. </div>
  67. </form>
  68. </template>
  69. <template slot='Login'>
  70. <p style='margin-top: 0;'>
  71. Login to create and post in threads.
  72. </p>
  73. <form @submit.prevent='doLogin'>
  74. <fancy-input
  75. v-model='login.username'
  76. :error='login.errors.username'
  77. placeholder='Username'
  78. width='100%'
  79. >
  80. </fancy-input>
  81. <fancy-input
  82. v-model='login.password'
  83. :error='login.errors.hash'
  84. placeholder='Password'
  85. type='password'
  86. width='100%'
  87. >
  88. </fancy-input>
  89. <div style='margin-top: 0.5rem;'>
  90. <loading-button
  91. class='button button--green button--margin'
  92. :loading='login.loading'
  93. @click='doLogin'
  94. >
  95. <span class='fa fa-unlock-alt' style='margin-right:0.25rem'></span> Log in
  96. </loading-button>
  97. <div class='button button--borderless' @click='closeAccountModal'>
  98. Cancel
  99. </div>
  100. </div>
  101. </form>
  102. </template>
  103. </tab-view>
  104. </modal-window>
  105. <header class='header'>
  106. <div class='header__group'>
  107. <router-link class='logo' to='/'>{{name}}</router-link>
  108. </div>
  109. <div class='header__group' :class='{ "header__group--show": showMenu }'>
  110. <template v-if='$store.state.username'>
  111. <notification-button></notification-button>
  112. <router-link
  113. to='/admin'
  114. class='button button--thin_text'
  115. v-if='$store.state.admin'
  116. >
  117. Admin settings
  118. </router-link>
  119. <router-link
  120. to='/settings'
  121. class='button button--thin_text'
  122. >
  123. Settings
  124. </router-link>
  125. <loading-button
  126. @click='logout'
  127. :loading='loadingLogout'
  128. class='button--thin_text'
  129. >
  130. Log out
  131. </loading-button>
  132. </template>
  133. <template v-else>
  134. <div class='button button--green button--thin_text' @click='showAccountModalTab(0)'>
  135. Sign up
  136. </div>
  137. <div class='button button--thin_text' @click='showAccountModalTab(1)'>
  138. Login
  139. </div>
  140. </template>
  141. <search-box header-bar='true'></search-box>
  142. </div>
  143. <div class='header__overlay' :class='{ "header__overlay--show": showMenu }' @click='toggleMenu'></div>
  144. <span class='fa fa-bars header__menu_button' @click='toggleMenu'></span>
  145. </header>
  146. <not-found v-show='$store.state.show404Page'></not-found>
  147. <router-view v-show='!$store.state.show404Page'></router-view>
  148. </div>
  149. </template>
  150. <script>
  151. import ModalWindow from './components/ModalWindow'
  152. import TabView from './components/TabView'
  153. import FancyInput from './components/FancyInput'
  154. import LoadingButton from './components/LoadingButton'
  155. import NotificationButton from './components/NotificationButton'
  156. import SearchBox from './components/SearchBox'
  157. import NotFound from './components/routes/NotFound'
  158. import AjaxErrorHandler from './assets/js/errorHandler'
  159. export default {
  160. name: 'app',
  161. components: {
  162. ModalWindow,
  163. TabView,
  164. FancyInput,
  165. LoadingButton,
  166. NotificationButton,
  167. SearchBox,
  168. NotFound
  169. },
  170. data () {
  171. return {
  172. signup: {
  173. username: '',
  174. password: '',
  175. confirmPassword: '',
  176. loading: false,
  177. errors: {
  178. username: '',
  179. hash: '',
  180. confirmPassword: ''
  181. }
  182. },
  183. login: {
  184. username: '',
  185. password: '',
  186. loading: false,
  187. errors: {
  188. username: '',
  189. hash: ''
  190. }
  191. },
  192. loadingLogout: false,
  193. showMenu: false,
  194. ajaxErrorHandler: AjaxErrorHandler(this.$store)
  195. }
  196. },
  197. computed: {
  198. name () {
  199. return this.$store.state.meta.name
  200. },
  201. showAccountModal: {
  202. get () { return this.$store.state.accountModal },
  203. set (val) {
  204. this.$store.commit('setAccountModalState', val);
  205. }
  206. },
  207. showAjaxErrorsModal: {
  208. get () { return this.$store.state.ajaxErrorsModal },
  209. set (val) { this.$store.commit('setAjaxErrorsModalState', val) }
  210. },
  211. showAccountTab : {
  212. get (val) { return this.$store.state.accountTabs },
  213. set (index) { this.$store.commit('setAccountTabs', index) }
  214. },
  215. categories() {
  216. return this.$store.state.meta.categories
  217. }
  218. },
  219. methods: {
  220. showAccountModalTab (index) {
  221. this.toggleMenu()
  222. this.showAccountModal = true
  223. this.showAccountTab = index
  224. },
  225. toggleMenu () {
  226. this.showMenu = !this.showMenu
  227. },
  228. logout () {
  229. this.toggleMenu()
  230. this.loadingLogout = true
  231. this.axios.post(
  232. '/api/v1/user/' +
  233. this.$store.state.username +
  234. '/logout'
  235. ).then(res => {
  236. this.loadingLogout = false
  237. this.$store.commit('setUsername', '')
  238. this.$store.commit('setAdmin', res.data.admin)
  239. socket.emit('accountEvent')
  240. this.$router.push('/')
  241. }).catch(err => {
  242. this.loadingLogout = false
  243. this.ajaxErrorHandler(err)
  244. })
  245. },
  246. clearSignup () {
  247. this.signup.username = ''
  248. this.signup.password = ''
  249. this.signup.confirmPassword = ''
  250. this.$store.commit('setToken', null)
  251. },
  252. clearSignupErrors () {
  253. this.signup.errors.username = ''
  254. this.signup.errors.hash = ''
  255. this.signup.errors.confirmPassword = ''
  256. },
  257. clearLogin () {
  258. this.login.username = ''
  259. this.login.password = ''
  260. },
  261. clearLoginErrors () {
  262. this.login.errors.username = ''
  263. this.login.errors.hash = ''
  264. },
  265. closeAccountModal () {
  266. this.showAccountModal = false
  267. this.clearLogin()
  268. this.clearSignup()
  269. this.clearLoginErrors()
  270. this.clearSignupErrors()
  271. },
  272. createAccount () {
  273. this.clearSignupErrors()
  274. let postParams = {
  275. username: this.signup.username,
  276. password: this.signup.password
  277. }
  278. if(this.$store.state.token) {
  279. postParams.admin = true
  280. postParams.token = this.$store.state.token
  281. }
  282. if(this.signup.password !== this.signup.confirmPassword) {
  283. this.signup.errors.confirmPassword = 'Passwords must match'
  284. } else {
  285. this.signup.loading = true
  286. this.axios.post('/api/v1/user', postParams).then(res => {
  287. this.signup.loading = false
  288. this.$store.commit('setUsername', res.data.username)
  289. this.$store.commit('setAdmin', res.data.admin)
  290. this.closeAccountModal()
  291. socket.emit('accountEvent')
  292. }).catch(e => {
  293. this.signup.loading = false
  294. this.ajaxErrorHandler(e, (error) => {
  295. let path = error.path
  296. if(this.signup.errors[path] !== undefined && this.signup.errors[path] !== undefined) {
  297. this.signup.errors[path] = error.message
  298. }
  299. })
  300. })
  301. }
  302. },
  303. doLogin () {
  304. this.clearSignupErrors()
  305. if(!this.login.username.trim().length) {
  306. this.login.errors.username = 'Username must not be blank'
  307. return
  308. }
  309. this.login.loading = true
  310. this.axios.post(`/api/v1/user/${this.login.username}/login`, {
  311. password: this.login.password
  312. }).then(res => {
  313. this.login.loading = false
  314. this.$store.commit('setUsername', res.data.username)
  315. this.$store.commit('setAdmin', res.data.admin)
  316. this.closeAccountModal()
  317. socket.emit('accountEvent')
  318. }).catch(e => {
  319. this.login.loading = false
  320. this.ajaxErrorHandler(e, (error) => {
  321. let path = error.path
  322. if(this.signup.errors[path] !== undefined && this.signup.errors[path] !== undefined) {
  323. this.signup.errors[path] = error.message
  324. }
  325. })
  326. })
  327. }
  328. },
  329. created () {
  330. this.axios.get('/api/v1/settings')
  331. .then(res => {
  332. this.$store.commit('setSettings', res.data)
  333. this.$store.dispatch('setTitle', this.$store.state.meta.title)
  334. }).catch(err => {
  335. if(err.response.data.errors[0].name === 'noSettings') {
  336. this.$router.push('/start')
  337. } else {
  338. this.ajaxErrorHandler(err)
  339. }
  340. })
  341. this.axios.get('/api/v1/category')
  342. .then(res => {
  343. this.$store.commit('addCategories', res.data)
  344. //Need categories to have loaded to set
  345. //the title of the index page
  346. //but if we're on another page (i.e. title is not set)
  347. //don't overwrite the title
  348. if(!this.$store.state.meta.title.length && this.$route.params.category) {
  349. let selectedCategory = this.$route.params.category.toUpperCase()
  350. let category = this.categories.find(c => c.value === selectedCategory)
  351. this.$store.dispatch('setTitle', category.name)
  352. }
  353. })
  354. .catch(this.ajaxErrorHandler)
  355. },
  356. watch: {
  357. $route () {
  358. this.showMenu = false
  359. },
  360. '$store.state.ajaxErrorsModal': function(val) {
  361. if(val) {
  362. this.$refs.ajaxErrorsModalButton.focus()
  363. }
  364. }
  365. }
  366. }
  367. </script>
  368. <style lang='scss'>
  369. @import url('https://fonts.googleapis.com/css?family=Lato:300,300i,400,400i,700');
  370. @import './assets/scss/variables.scss';
  371. @import './assets/scss/elementStyles.scss';
  372. html, body {
  373. width: 100%;
  374. height: 100%;
  375. margin: 0;
  376. padding: 0;
  377. color: $color__text--primary;
  378. @include text;
  379. }
  380. * {
  381. box-sizing: border-box;
  382. }
  383. .route_container {
  384. width: 80%;
  385. max-width: 1250px;
  386. margin: 0 auto;
  387. margin-top: 2rem;
  388. padding-bottom: 2rem;
  389. }
  390. #app {
  391. padding-top: 4.5rem;
  392. height: 100%;
  393. }
  394. .header {
  395. width: 100%;
  396. padding: 0.5rem 2rem;
  397. position: fixed;
  398. top: 0;
  399. z-index: 2;
  400. display: flex;
  401. align-items: center;
  402. justify-content: space-between;
  403. border-bottom: 0.125rem solid $color__gray--primary;
  404. background-color: #fff;
  405. @at-root #{&}__group {
  406. display: flex;
  407. align-items: center;
  408. > * { margin: 0 0.5rem; }
  409. > *:first-child { margin-left: 0; }
  410. > *:last-child { margin-right: 0; }
  411. }
  412. @at-root #{&}__menu_button {
  413. position: fixed;
  414. left: 1rem;
  415. z-index: 1;
  416. font-size: 1.5rem;
  417. top: 1rem;
  418. display: none;
  419. }
  420. @at-root #{&}__overlay {
  421. width: 100%;
  422. height: 100%;
  423. position: fixed;
  424. top: 0;
  425. left: 0;
  426. z-index: 1;
  427. pointer-events: none;
  428. opacity: 0;
  429. background-color: hsla(215, 13%, 25%, 0.5);
  430. transition: all 0.4s;
  431. }
  432. }
  433. .logo {
  434. @include text($font--role-emphasis, 2rem, 600);
  435. @include user-select(none);
  436. cursor: pointer;
  437. background: none;
  438. white-space: nowrap;
  439. overflow: hidden;
  440. text-overflow: ellipsis;
  441. max-width: 20rem;
  442. &:hover, &:visited, &:active {
  443. outline: none;
  444. color: $color__text--primary;
  445. }
  446. }
  447. @media (max-width: 870px) {
  448. .route_container {
  449. width: calc(100% - 2rem);
  450. margin: 0 1rem;
  451. margin-top: 0rem;
  452. }
  453. .logo {
  454. position: relative;
  455. z-index: 2;
  456. max-width: calc(100vw - 7rem);
  457. }
  458. .header__menu_button {
  459. display: inline-block;
  460. cursor: pointer;
  461. }
  462. .header__overlay--show {
  463. pointer-events: all;
  464. opacity: 1;
  465. }
  466. .header__group:first-child {
  467. margin-left: 1rem;
  468. }
  469. .header__group:nth-child(2) {
  470. position: fixed;
  471. padding-top: 1.5rem;
  472. width: 17rem;
  473. display: flex;
  474. flex-direction: column;
  475. z-index: 2;
  476. background: #fff;
  477. top: 0;
  478. left: calc(-100% - 2rem);
  479. height: 100%;
  480. box-shadow: none;
  481. transition: left 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.4s ease-in;
  482. > .button {
  483. width: 100%;
  484. border-radius: 0;
  485. margin: 0;
  486. margin-bottom: 1rem;
  487. }
  488. &::before {
  489. position: absolute;
  490. top: 0;
  491. left: 0;
  492. width: 100%;
  493. height: 0.3rem;
  494. content: '';
  495. background: linear-gradient(to right, hsl(200, 98%, 43%), hsla(193, 98%, 48%, 1));
  496. }
  497. }
  498. .header__group:nth-child(2).header__group--show {
  499. left: 0;
  500. box-shadow: 0 0 1rem rgba(0, 0, 0, 0.4);
  501. }
  502. .search_box {
  503. margin: 0;
  504. display: inline-block;
  505. }
  506. }
  507. </style>