NotificationButton.vue 8.7 KB

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