NotificationButton.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <template>
  2. <div class='notification_button'>
  3. <div
  4. class='notification_button__overlay'
  5. :class='{ "notification_button__overlay--show" : showMenu}'
  6. @click='setShowMenu(false)'
  7. ></div>
  8. <button
  9. class='button notification_button__button'
  10. :class='{ "notification_button__button--shake": shake }'
  11. @click='setShowMenu(!showMenu)'
  12. >
  13. <span>Notifications</span>
  14. <span
  15. class='notification_button__button__count'
  16. :class='{
  17. "notification_button__button__count--none": !unreadCount,
  18. "notification_button__button__count--two_figure": unreadCount > 9,
  19. "notification_button__button__count--three_figure": unreadCount > 99
  20. }'
  21. >{{unreadCountText}}</span>
  22. </button>
  23. <div
  24. class='notification_button__menu_group'
  25. :class='{ "notification_button__menu_group--show" : showMenu}'
  26. >
  27. <div class='notification_button__big_triangle'></div>
  28. <div
  29. class='notification_button__small_triangle'
  30. :class='{ "notification_button__small_triangle--empty": !notifications.length}'
  31. ></div>
  32. <div class='notification_button__menu'>
  33. <div
  34. v-for='(notification, index) in notifications'
  35. class='notification_button__menu__item'
  36. :class='{
  37. "notification_button__menu__item--uninteracted": !notification.interacted,
  38. "notification_button__menu__item--no_border": index > 2
  39. }'
  40. @click='click(notification)'
  41. >
  42. <template v-if='notification.type === "mention"'>
  43. <div class='notification_button__menu__item__header'>
  44. <span>New mention</span>
  45. <span>
  46. <span class='notification_button__menu__item__header__date'>{{notification.createdAt | formatDate }}</span>
  47. <span
  48. class='notification_button__menu__item__header__close'
  49. @click.stop='deleteNotification(notification.id)'
  50. >&times;</span>
  51. </span>
  52. </div>
  53. <div>
  54. <span class='notification_button__menu__item__link'>
  55. {{notification.PostNotification.User.username}}
  56. </span>
  57. wrote
  58. "{{notification.PostNotification.Post.content | stripTags | truncate(50)}}"
  59. </div>
  60. </template>
  61. <template v-if='notification.type === "reply"'>
  62. <div class='notification_button__menu__item__header'>
  63. <span>Reply to your post</span>
  64. <span>
  65. <span class='notification_button__menu__item__header__date'>{{notification.createdAt | formatDate }}</span>
  66. <span
  67. class='notification_button__menu__item__header__close'
  68. @click.stop='deleteNotification(notification.id)'
  69. >&times;</span>
  70. </span>
  71. </div>
  72. <div>
  73. <span class='notification_button__menu__item__link'>
  74. {{notification.PostNotification.User.username}}
  75. </span>
  76. replied
  77. "{{notification.PostNotification.Post.content | stripTags | truncate(50)}}"
  78. </div>
  79. </template>
  80. </div>
  81. <div class='notification_button__menu__empty' v-if='!notifications.length'>
  82. <span>{{emojis[emojiIndex % 6]}}</span>
  83. No notifications
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </template>
  89. <script>
  90. import AjaxErrorHandler from '../assets/js/errorHandler'
  91. export default {
  92. name: 'NotificationButton',
  93. data () {
  94. return {
  95. unreadCount: 0,
  96. notifications: [],
  97. showMenu: false,
  98. shake: false,
  99. emojis: ['😢', '🤷', '😘', '😒', '😔', '💩'],
  100. emojiIndex: Math.round(Math.random()*5)
  101. }
  102. },
  103. computed: {
  104. unreadCountText () {
  105. if(this.unreadCount > 99) {
  106. return '99+'
  107. } else {
  108. return this.unreadCount
  109. }
  110. }
  111. },
  112. methods: {
  113. setShowMenu (val) {
  114. this.showMenu = val
  115. if(val) {
  116. this.resetUnreadCount()
  117. } else {
  118. setTimeout(_ => {
  119. this.emojiIndex++
  120. }, 200)
  121. }
  122. },
  123. getIndexById (id) {
  124. let index
  125. this.notifications.forEach((notification, i) => {
  126. if(notification.id === id) {
  127. index = i
  128. }
  129. })
  130. return index
  131. },
  132. getNotifications () {
  133. this.axios
  134. .get('/api/v1/notification')
  135. .then(res => {
  136. this.notifications = res.data.Notifications
  137. this.unreadCount = res.data.unreadCount
  138. })
  139. .catch(AjaxErrorHandler(this.$store))
  140. },
  141. resetUnreadCount () {
  142. this.axios
  143. .put('/api/v1/notification')
  144. .then(res => {
  145. this.unreadCount = 0
  146. })
  147. .catch(AjaxErrorHandler(this.$store))
  148. },
  149. deleteNotification (id) {
  150. let index = this.getIndexById(id)
  151. this.axios
  152. .delete('/api/v1/notification/' + id)
  153. .then(res => {
  154. this.notifications.splice(index, 1)
  155. })
  156. .catch(AjaxErrorHandler(this.$store))
  157. },
  158. setInteracted (id) {
  159. let index = this.getIndexById(id)
  160. let item = this.notifications[index]
  161. this.axios
  162. .put('/api/v1/notification/' + id)
  163. .then(res => {
  164. this.$set(
  165. this.notifications,
  166. index,
  167. Object.assign(item, { interacted: true })
  168. )
  169. })
  170. .catch(AjaxErrorHandler(this.$store))
  171. },
  172. click (notification) {
  173. if(!notification.interacted) {
  174. this.setInteracted(notification.id)
  175. }
  176. if(notification.type === 'mention' || notification.type === 'reply') {
  177. this.$router.push('/p/' + notification.PostNotification.Post.id)
  178. } else if(notification.type === 'reply') {
  179. this.$router.push('/p/' + notification.PostNotification.Post.id)
  180. }
  181. this.setShowMenu(false)
  182. }
  183. },
  184. created () {
  185. if(this.$store.state.username) this.getNotifications()
  186. socket.on('notification', notification => {
  187. this.unreadCount++
  188. this.notifications.unshift(notification)
  189. this.shake = true
  190. setTimeout(_ => {
  191. this.shake = false
  192. }, 1000)
  193. })
  194. },
  195. watch: {
  196. '$store.state.username': 'getNotifications'
  197. }
  198. }
  199. </script>
  200. <style lang='scss' scoped>
  201. @import '../assets/scss/variables.scss';
  202. @keyframes shake {
  203. 0% {
  204. position: relative;
  205. left: 0;
  206. }
  207. 25% {
  208. position: relative;
  209. left: -1rem;
  210. }
  211. 75% {
  212. position: relative;
  213. left: 1rem;
  214. }
  215. 100% {
  216. left: 0rem;
  217. }
  218. }
  219. .notification_button {
  220. position: relative;
  221. @at-root #{&}__overlay {
  222. width: 100%;
  223. height: 100%;
  224. top: 0;
  225. left: 0;
  226. position: fixed;
  227. z-index: 5;
  228. pointer-events: none;
  229. @at-root #{&}--show {
  230. pointer-events: all;
  231. }
  232. }
  233. @at-root #{&}__menu_group {
  234. position: relative;
  235. top: -3rem;
  236. pointer-events: none;
  237. opacity: 0;
  238. transition: opacity 0.2s, top 0.2s;
  239. @at-root #{&}--show {
  240. pointer-events: all;
  241. opacity: 1;
  242. top: -2.5rem;
  243. }
  244. }
  245. @at-root #{&}__big_triangle {
  246. width: 1rem;
  247. height: 1rem;
  248. background-color: #fafafa;
  249. transform: rotate(45deg);
  250. position: absolute;
  251. box-shadow: 5px 5px 10px 0px rgba(0, 0, 0, 0.75);
  252. top: 2.4rem;
  253. border-radius: 0.125rem 0 0 0;
  254. border: 0.125rem solid $color__gray--primary;
  255. left: calc(50% - 1.414rem /2);
  256. z-index: 6;
  257. }
  258. @at-root #{&}__small_triangle {
  259. width: 0;
  260. left: calc(50% - 1.414rem / 2 - .125rem);
  261. height: 0;
  262. border-left: 0.625rem solid transparent;
  263. top: 2.4rem;
  264. border-right: 0.625rem solid transparent;
  265. border-bottom: 0.625rem solid #fafafa;
  266. position: absolute;
  267. z-index: 8;
  268. }
  269. @at-root #{&}__menu {
  270. left: calc(-50% - 1.25rem);
  271. position: absolute;
  272. top: 2.9rem;
  273. background-color: #fafafa;
  274. width: 20rem;
  275. border-radius: 0.25rem;
  276. border: 0.125rem solid $color__gray--darker;
  277. box-shadow: 0 7px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
  278. min-height: 8rem;
  279. max-height: 15rem;
  280. overflow-y: auto;
  281. z-index: 7;
  282. @at-root #{&}__empty {
  283. background-color: #fafafa;
  284. display: flex;
  285. flex-direction: column;
  286. align-items: center;
  287. padding: 2rem;
  288. height: 8rem;
  289. justify-content: center;
  290. font-size: 1rem;
  291. user-select: none;
  292. cursor: default;
  293. transition: none;
  294. color: $color__gray--darkest;
  295. span {
  296. font-size: 2rem;
  297. color: $color__gray--darker;
  298. margin-bottom: 0.5rem;
  299. }
  300. }
  301. @at-root #{&}__item {
  302. @at-root #{&}--no_border &:last-child {
  303. border: none;
  304. }
  305. padding: 0.5rem;
  306. border-bottom: thin solid $color__gray--primary;
  307. cursor: default;
  308. background-color: #fff;
  309. transition: background-color 0.2s;
  310. &:hover {
  311. background-color: $color__lightgray--primary;
  312. }
  313. @at-root #{&}--uninteracted {
  314. background-color: rgba(13, 71, 161, 0.1);
  315. border-bottom-color: $color__gray--darkest;
  316. &:hover {
  317. background-color: rgba(13, 71, 161, 0.2);
  318. }
  319. }
  320. @at-root #{&}__link {
  321. font-weight: 400;
  322. cursor: pointer;
  323. }
  324. @at-root #{&}__header {
  325. display: flex;
  326. justify-content: space-between;
  327. font-size: 0.9rem;
  328. @at-root #{&}__date {
  329. color: $color__text--secondary;
  330. }
  331. @at-root #{&}__close {
  332. background-color: $color__gray--darkest;
  333. height: 0.9rem;
  334. width: 0.9rem;
  335. cursor: pointer;
  336. display: inline-flex;
  337. border-radius: 100%;
  338. margin-left: 0.25rem;
  339. align-items: center;
  340. justify-content: center;
  341. padding: 0;
  342. color: #fff;
  343. position: relative;
  344. top: 0.0625rem;
  345. line-height: 1;
  346. transition: all 0.2s;
  347. &:hover {
  348. filter: brightness(0.9);
  349. }
  350. }
  351. }
  352. }
  353. }
  354. @at-root #{&}__button {
  355. position: relative;
  356. padding-right: 2.5rem;
  357. @at-root #{&}--shake {
  358. animation-name: shake;
  359. animation-iteration-count: 4;
  360. animation-duration: 0.25s;
  361. animation-timing-function: ease-in-out;
  362. }
  363. @at-root #{&}__count {
  364. position: absolute;
  365. background-color: $color__blue--primary;
  366. line-height: 1;
  367. margin-left: 0.25rem;
  368. color: #fff;
  369. top: 0.35rem;
  370. right: 0.5rem;
  371. border-radius: 100%;
  372. height: 1rem;
  373. width: 1rem;
  374. display: inline-flex;
  375. align-items: center;
  376. padding: 0.75rem;
  377. font-size: 0.9rem;
  378. justify-content: center;
  379. transition: all 0.2s;
  380. @at-root #{&}--none {
  381. background-color: rgba(white, 0.75);
  382. font-weight: 300;
  383. color: initial;
  384. border: 0.0125rem solid transparent;
  385. background-color: $color__gray--primary;
  386. padding: calc(0.75rem - 4*0.0125rem);
  387. }
  388. @at-root #{&}--two_figure {
  389. font-size: 0.8rem;
  390. }
  391. @at-root #{&}--three_figure {
  392. font-size: 0.7rem;
  393. }
  394. }
  395. }
  396. }
  397. </style>