ThreadNew.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <template>
  2. <div class='route_container thread_new'>
  3. <div class='h1'>Post new thread</div>
  4. <div class='thread_meta_info'>
  5. <div class='thread_meta_info__text'>Enter the thread title and the category to post it in</div>
  6. <div class='thread_meta_info__form'>
  7. <select-button v-model='selectedCategory' :options='categories'></select-button>
  8. <fancy-input
  9. placeholder='Thread title'
  10. v-model='name'
  11. :error='errors.name'
  12. class='thread_meta_info__title'
  13. large='true'
  14. width='15rem'
  15. ></fancy-input>
  16. <button
  17. class='thread_meta_info__add_poll button button--thin_text'
  18. v-if='!showPoll'
  19. @click='togglePoll(true)'
  20. >Add poll</button>
  21. </div>
  22. <transition name='slide'>
  23. <div class='thread_meta_info__poll' v-if='showPoll'>
  24. <div class='thread_meta_info__poll__top_bar'>
  25. <fancy-input
  26. class='thread_meta_info__poll__question'
  27. v-model='pollQuestion'
  28. placeholder='Poll question'
  29. width='20rem'
  30. :large='true'
  31. :error='errors.pollQuestion'
  32. ></fancy-input>
  33. <button class='button button--thin_text button--borderless' @click='removePoll'>
  34. Remove poll
  35. </button>
  36. </div>
  37. <div>
  38. <div class='thread_meta_info__poll__answer' v-for='(pollAnswer, $index) in pollAnswers'>
  39. <fancy-input
  40. v-model='pollAnswer.answer'
  41. width='15rem'
  42. :large='true'
  43. :placeholder='"Answer " + ($index+1)'
  44. ></fancy-input>
  45. <span @click='removePollAnswer($index)' title='Remove answer'>&times;</span>
  46. </div>
  47. <div class='thread_meta_info__form'>
  48. <fancy-input
  49. v-model='newPollAnswer'
  50. placeholder='Option/answer for poll'
  51. style='display: inline-block; margin-right: 0.5rem;'
  52. width='15rem'
  53. :large='true'
  54. :error='errors.pollAnswer'
  55. ></fancy-input>
  56. <button class='button button--thin_text' @click='addPollAnswer'>Add answer</button>
  57. </div>
  58. </div>
  59. </div>
  60. </transition>
  61. </div>
  62. <div
  63. class='editor'
  64. :class='{
  65. "editor--focus": focusInput,
  66. "editor--error": errors.content
  67. }'
  68. >
  69. <div class='editor__input'>
  70. <div class='editor__format_bar editor__format_bar--editor'>
  71. editor
  72. </div>
  73. <input-editor-core
  74. v-model='editor'
  75. @mentions='setMentions'
  76. @focus='setFocusInput(true)'
  77. @blur='setFocusInput(false)'
  78. ></input-editor-core>
  79. </div>
  80. <div class='editor__preview'>
  81. <div class='editor__format_bar editor__format_bar--preview'>
  82. preview
  83. </div>
  84. <input-editor-preview :value='editor' :mentions='mentions'></input-editor-preview>
  85. </div>
  86. </div>
  87. <error-tooltip :error='errors.content' class='editor_error'></error-tooltip>
  88. <loading-button class='button--green submit' :loading='loading' @click='postThread'>Post thread</loading-button>
  89. </div>
  90. </template>
  91. <script>
  92. import InputEditorCore from '../InputEditorCore'
  93. import InputEditorPreview from '../InputEditorPreview'
  94. import FancyInput from '../FancyInput'
  95. import SelectButton from '../SelectButton'
  96. import LoadingButton from '../LoadingButton'
  97. import ErrorTooltip from '../ErrorTooltip'
  98. import AjaxErrorHandler from '../../assets/js/errorHandler'
  99. import logger from '../../assets/js/logger'
  100. export default {
  101. name: 'ThreadNew',
  102. components: {
  103. InputEditorCore,
  104. InputEditorPreview,
  105. SelectButton,
  106. FancyInput,
  107. LoadingButton,
  108. ErrorTooltip
  109. },
  110. data () {
  111. return {
  112. selectedCategory: this.$store.state.category.selectedCategory,
  113. editor: '',
  114. mentions: [],
  115. name: '',
  116. loading: false,
  117. focusInput: false,
  118. errors: {
  119. content: '',
  120. name: '',
  121. pollQuestion: '',
  122. pollAnswer: ''
  123. },
  124. showPoll: false,
  125. pollQuestion: '',
  126. newPollAnswer: '',
  127. pollAnswers: []
  128. }
  129. },
  130. computed: {
  131. categories () {
  132. return this.$store.getters.categoriesWithoutAll
  133. }
  134. },
  135. methods: {
  136. togglePoll (val) {
  137. if(val !== undefined) {
  138. this.showPoll = val
  139. } else {
  140. this.showPoll = !this.showPoll
  141. }
  142. },
  143. addPollAnswer () {
  144. if(!this.newPollAnswer.trim().length) return
  145. this.pollAnswers.push({ answer: this.newPollAnswer })
  146. this.newPollAnswer = ''
  147. },
  148. removePollAnswer ($index) {
  149. this.pollAnswers.splice($index, 1)
  150. },
  151. removePoll () {
  152. this.pollQuestion = ''
  153. this.pollAnswers = []
  154. this.newPollAnswer = ''
  155. this.togglePoll()
  156. },
  157. setErrors (errors) {
  158. errors.forEach(error => {
  159. this.errors[error.name] = error.error
  160. })
  161. },
  162. clearErrors () {
  163. this.errors.content = ''
  164. this.errors.name = ''
  165. this.errors.pollQuestion = ''
  166. this.errors.pollAnswer = ''
  167. },
  168. hasDuplicates (array, cb) {
  169. if(cb) array = array.map(cb)
  170. return array.length !== (new Set(array)).size
  171. },
  172. postThread () {
  173. let thread
  174. let errors = []
  175. this.clearErrors()
  176. if(!this.editor.trim().length) {
  177. errors.push({name: 'content', error: 'Post content cannot be blank'})
  178. } if(!this.name.trim().length) {
  179. errors.push({name: 'name', error: 'Cannot be blank'})
  180. } if(this.showPoll && !this.pollQuestion.trim().length) {
  181. errors.push({name: 'pollQuestion', error: 'Cannot be blank'})
  182. } if (this.showPoll && this.pollAnswers.length < 2) {
  183. errors.push({name: 'pollAnswer', error: 'You need at least 2 answers'})
  184. } if (this.showPoll && this.hasDuplicates(this.pollAnswers, i => i.answer)) {
  185. errors.push({name: 'pollAnswer', error: 'Your answers can\'t contain any duplicates'})
  186. } if(errors.length) {
  187. this.setErrors(errors)
  188. return
  189. }
  190. this.loading = true
  191. this.axios.post('/api/v1/thread', {
  192. name: this.name,
  193. category: this.selectedCategory
  194. }).then(res => {
  195. thread = res.data
  196. let ajax = []
  197. ajax.push(
  198. this.axios.post('/api/v1/post', {
  199. threadId: res.data.id,
  200. content: this.editor,
  201. mentions: this.mentions
  202. })
  203. )
  204. if(this.showPoll) {
  205. ajax.push(
  206. this.axios.post('/api/v1/poll', {
  207. question: this.pollQuestion,
  208. answers: this.pollAnswers.map(a => a.answer),
  209. threadId: res.data.id
  210. })
  211. )
  212. }
  213. return Promise.all(ajax)
  214. }).then(res => {
  215. this.loading = false
  216. this.$router.push(`/thread/${thread.slug}/${thread.id}/0`)
  217. }).catch(e => {
  218. this.loading = false
  219. AjaxErrorHandler(this.$store)(e, (error, errors) => {
  220. let path = error.path
  221. if(this.errors[path] !== undefined) {
  222. this.errors[path] = error.message
  223. } else {
  224. errors.push(error.message)
  225. }
  226. })
  227. })
  228. },
  229. setFocusInput (val) {
  230. this.focusInput = val
  231. },
  232. setMentions (mentions) {
  233. this.mentions = mentions
  234. }
  235. },
  236. watch: {
  237. '$store.state.username' (username) {
  238. if(!username) {
  239. this.$router.push('/')
  240. }
  241. }
  242. },
  243. mounted () {
  244. this.$store.dispatch('setTitle', 'new thread')
  245. logger('threadNew')
  246. },
  247. beforeRouteEnter (to, from, next) {
  248. next(vm => {
  249. if(!vm.$store.state.username) {
  250. vm.$store.commit('setAccountModalState', true);
  251. next('/')
  252. }
  253. })
  254. }
  255. }
  256. </script>
  257. <style lang='scss'>
  258. @import '../../assets/scss/variables.scss';
  259. .thread_new {
  260. margin-top: 1rem;
  261. }
  262. .thread_meta_info {
  263. background-color: #fff;
  264. border: thin solid $color__gray--darker;
  265. border-radius: 0.25rem;
  266. padding: 1rem;
  267. margin: 1rem 0;
  268. @at-root #{&}__title {
  269. margin: 0 0.5rem;
  270. margin-top: 0.5rem;
  271. display: inline-block;
  272. }
  273. @at-root #{&}__form {
  274. display: flex;
  275. align-items: baseline;
  276. }
  277. @at-root #{&}__add_poll {
  278. margin-top: 0.5rem;
  279. }
  280. @at-root #{&}__text {
  281. margin-bottom: 0.5rem;
  282. }
  283. @at-root #{&}__poll {
  284. border-top: thin solid $color__gray--primary;
  285. margin-top: 1rem;
  286. padding-top: 0.75rem;
  287. position: relative;
  288. @at-root #{&}__top_bar {
  289. display: flex;
  290. justify-content: space-between;
  291. align-items: baseline;
  292. }
  293. @at-root #{&}__answer {
  294. display: flex;
  295. align-items: baseline;
  296. & > span {
  297. opacity: 0;
  298. pointer-events: none;
  299. transition: all 0.1s;
  300. font-size: 1.5rem;
  301. margin-left: 0.5rem;
  302. cursor: pointer;
  303. @include user-select(none);
  304. }
  305. &:hover > span {
  306. opacity: 1;
  307. pointer-events: all;
  308. }
  309. }
  310. }
  311. }
  312. .submit {
  313. margin-top: 1rem;
  314. }
  315. .editor {
  316. display: flex;
  317. background-color: #fff;
  318. border-radius: 0.25rem;
  319. border: thin solid $color__gray--darker;
  320. transition: all 0.2s;
  321. @at-root #{&}--focus {
  322. border: thin solid $color__gray--darkest;
  323. }
  324. @at-root #{&}--error {
  325. border: thin solid $color__red--primary;
  326. }
  327. @at-root #{&}__format_bar {
  328. height: 2.5rem;
  329. background-color: $color__gray--primary;
  330. display: flex;
  331. padding-right: 1rem;
  332. padding-bottom: 0.25rem;
  333. justify-content: flex-end;
  334. align-items: center;
  335. font-variant: small-caps;
  336. @at-root #{&}--preview {
  337. border-radius: 0 0.25rem 0 0;
  338. }
  339. @at-root #{&}--editor {
  340. border-radius: 0.25rem 0 0 0;
  341. }
  342. }
  343. @at-root #{&}__input {
  344. width: 50%;
  345. position: relative;
  346. .input_editor_core__format_bar {
  347. left: 0rem;
  348. }
  349. .input_editor_core textarea {
  350. height: 14rem;
  351. }
  352. }
  353. @at-root #{&}__preview {
  354. border-left: 1px solid $color__gray--darker;
  355. width: 50%;
  356. div.input_editor_preview__markdownHTML {
  357. height: 14.1rem;
  358. word-break: break-all;
  359. }
  360. }
  361. }
  362. .editor_error {
  363. width: 100%;
  364. background: #fff;
  365. margin-top: 0.5rem;
  366. border-radius: 0.2rem;
  367. border: thin solid $color__red--primary;
  368. &.error_tooltip--show {
  369. max-height: 4rem;
  370. padding: 0.5rem;
  371. }
  372. }
  373. @media (max-width: 600px) {
  374. .thread_meta_info {
  375. @at-root #{&}__form {
  376. flex-direction: column;
  377. }
  378. @at-root #{&}__title.fancy_input {
  379. margin: 0;
  380. margin-top: 0.5rem;
  381. }
  382. @at-root #{&}__poll__top_bar .button {
  383. position: absolute;
  384. bottom: 0;
  385. right: 0;
  386. }
  387. @at-root #{&}__poll__question {
  388. width: 100%;
  389. > div, input {
  390. width: 100% !important;
  391. }
  392. }
  393. }
  394. .editor {
  395. flex-direction: column;
  396. overflow-x: hidden;
  397. @at-root #{&}__input, #{&}__preview {
  398. border: 0;
  399. width: 100%;
  400. }
  401. }
  402. }
  403. </style>