Profile.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. <template>
  2. <div class="content profile-tab">
  3. <h4 class="section-title">Change Profile</h4>
  4. <p class="section-description">
  5. Edit your public profile so users can find out more about you
  6. </p>
  7. <hr class="section-horizontal-rule" />
  8. <div
  9. class="control is-expanded avatar-selection-outer-container"
  10. v-if="modifiedUser.avatar"
  11. >
  12. <label>Avatar</label>
  13. <div id="avatar-selection-inner-container">
  14. <profile-picture
  15. :avatar="modifiedUser.avatar"
  16. :name="
  17. modifiedUser.name
  18. ? modifiedUser.name
  19. : modifiedUser.username
  20. "
  21. />
  22. <div class="select">
  23. <select v-model="modifiedUser.avatar.type">
  24. <option value="gravatar">Using Gravatar</option>
  25. <option value="initials">Based on initials</option>
  26. </select>
  27. </div>
  28. <div
  29. class="select"
  30. v-if="modifiedUser.avatar.type === 'initials'"
  31. >
  32. <select v-model="modifiedUser.avatar.color">
  33. <option value="blue">Blue</option>
  34. <option value="orange">Orange</option>
  35. <option value="green">Green</option>
  36. <option value="purple">Purple</option>
  37. <option value="teal">Teal</option>
  38. </select>
  39. </div>
  40. </div>
  41. </div>
  42. <p class="control is-expanded margin-top-zero">
  43. <label for="name">Name</label>
  44. <input
  45. class="input"
  46. id="name"
  47. type="text"
  48. placeholder="Enter name here..."
  49. maxlength="64"
  50. v-model="modifiedUser.name"
  51. />
  52. <span v-if="modifiedUser.name" class="character-counter"
  53. >{{ modifiedUser.name.length }}/64</span
  54. >
  55. </p>
  56. <p class="control is-expanded">
  57. <label for="location">Location</label>
  58. <input
  59. class="input"
  60. id="location"
  61. type="text"
  62. placeholder="Enter location here..."
  63. maxlength="50"
  64. v-model="modifiedUser.location"
  65. />
  66. <span v-if="modifiedUser.location" class="character-counter"
  67. >{{ modifiedUser.location.length }}/50</span
  68. >
  69. </p>
  70. <p class="control is-expanded">
  71. <label for="bio">Bio</label>
  72. <textarea
  73. class="textarea"
  74. id="bio"
  75. placeholder="Enter bio here..."
  76. maxlength="200"
  77. autocomplete="off"
  78. v-model="modifiedUser.bio"
  79. />
  80. <span v-if="modifiedUser.bio" class="character-counter"
  81. >{{ modifiedUser.bio.length }}/200</span
  82. >
  83. </p>
  84. <save-button ref="saveButton" @clicked="saveChanges()" />
  85. </div>
  86. </template>
  87. <script>
  88. import { mapState, mapActions, mapGetters } from "vuex";
  89. import Toast from "toasters";
  90. import ProfilePicture from "@/components/ProfilePicture.vue";
  91. import SaveButton from "@/components/SaveButton.vue";
  92. import validation from "@/validation";
  93. export default {
  94. components: { ProfilePicture, SaveButton },
  95. computed: {
  96. ...mapState({
  97. userId: state => state.user.auth.userId,
  98. originalUser: state => state.settings.originalUser,
  99. modifiedUser: state => state.settings.modifiedUser
  100. }),
  101. ...mapGetters({
  102. socket: "websockets/getSocket"
  103. })
  104. },
  105. methods: {
  106. saveChanges() {
  107. const nameChanged =
  108. this.modifiedUser.name !== this.originalUser.name;
  109. const locationChanged =
  110. this.modifiedUser.location !== this.originalUser.location;
  111. const bioChanged = this.modifiedUser.bio !== this.originalUser.bio;
  112. const avatarChanged =
  113. this.modifiedUser.avatar.type !==
  114. this.originalUser.avatar.type ||
  115. this.modifiedUser.avatar.color !==
  116. this.originalUser.avatar.color;
  117. if (nameChanged) this.changeName();
  118. if (locationChanged) this.changeLocation();
  119. if (bioChanged) this.changeBio();
  120. if (avatarChanged) this.changeAvatar();
  121. if (
  122. !avatarChanged &&
  123. !bioChanged &&
  124. !locationChanged &&
  125. !nameChanged
  126. ) {
  127. this.$refs.saveButton.handleFailedSave();
  128. new Toast("Please make a change before saving.");
  129. }
  130. },
  131. changeName() {
  132. this.modifiedUser.name = this.modifiedUser.name
  133. .replaceAll(/ +/g, " ")
  134. .trim();
  135. const { name } = this.modifiedUser;
  136. if (!validation.isLength(name, 1, 64))
  137. return new Toast("Name must have between 1 and 64 characters.");
  138. if (!validation.regex.name.test(name))
  139. return new Toast(
  140. "Invalid name format. Only letters, numbers, spaces, apostrophes, underscores and hyphens are allowed."
  141. );
  142. if (name.replaceAll(/[ .'_-]/g, "").length === 0)
  143. return new Toast(
  144. "Invalid name format. Only letters, numbers, spaces, apostrophes, underscores and hyphens are allowed, and there has to be at least one letter or number."
  145. );
  146. this.$refs.saveButton.status = "disabled";
  147. return this.socket.dispatch(
  148. "users.updateName",
  149. this.userId,
  150. name,
  151. res => {
  152. if (res.status !== "success") {
  153. new Toast(res.message);
  154. this.$refs.saveButton.handleFailedSave();
  155. } else {
  156. new Toast("Successfully changed name");
  157. this.updateOriginalUser({
  158. property: "name",
  159. value: name
  160. });
  161. this.$refs.saveButton.handleSuccessfulSave();
  162. }
  163. }
  164. );
  165. },
  166. changeLocation() {
  167. const { location } = this.modifiedUser;
  168. if (!validation.isLength(location, 0, 50))
  169. return new Toast(
  170. "Location must have between 0 and 50 characters."
  171. );
  172. this.$refs.saveButton.status = "disabled";
  173. return this.socket.dispatch(
  174. "users.updateLocation",
  175. this.userId,
  176. location,
  177. res => {
  178. if (res.status !== "success") {
  179. new Toast(res.message);
  180. this.$refs.saveButton.handleFailedSave();
  181. } else {
  182. new Toast("Successfully changed location");
  183. this.updateOriginalUser({
  184. property: "location",
  185. value: location
  186. });
  187. this.$refs.saveButton.handleSuccessfulSave();
  188. }
  189. }
  190. );
  191. },
  192. changeBio() {
  193. const { bio } = this.modifiedUser;
  194. if (!validation.isLength(bio, 0, 200))
  195. return new Toast("Bio must have between 0 and 200 characters.");
  196. this.$refs.saveButton.status = "disabled";
  197. return this.socket.dispatch(
  198. "users.updateBio",
  199. this.userId,
  200. bio,
  201. res => {
  202. if (res.status !== "success") {
  203. new Toast(res.message);
  204. this.$refs.saveButton.handleFailedSave();
  205. } else {
  206. new Toast("Successfully changed bio");
  207. this.updateOriginalUser({
  208. property: "bio",
  209. value: bio
  210. });
  211. this.$refs.saveButton.handleSuccessfulSave();
  212. }
  213. }
  214. );
  215. },
  216. changeAvatar() {
  217. const { avatar } = this.modifiedUser;
  218. this.$refs.saveButton.status = "disabled";
  219. return this.socket.dispatch(
  220. "users.updateAvatar",
  221. this.userId,
  222. avatar,
  223. res => {
  224. if (res.status !== "success") {
  225. new Toast(res.message);
  226. this.$refs.saveButton.handleFailedSave();
  227. } else {
  228. new Toast("Successfully updated avatar");
  229. this.updateOriginalUser({
  230. property: "avatar",
  231. value: avatar
  232. });
  233. this.$refs.saveButton.handleSuccessfulSave();
  234. }
  235. }
  236. );
  237. },
  238. ...mapActions("settings", ["updateOriginalUser"])
  239. }
  240. };
  241. </script>
  242. <style lang="less" scoped>
  243. .content .control {
  244. margin-bottom: 15px;
  245. }
  246. .character-counter {
  247. height: initial;
  248. }
  249. .avatar-selection-outer-container {
  250. display: flex;
  251. flex-direction: column;
  252. align-items: flex-start;
  253. .select:after {
  254. border-color: var(--primary-color);
  255. }
  256. #avatar-selection-inner-container {
  257. display: flex;
  258. align-items: center;
  259. margin-top: 5px;
  260. .select {
  261. margin-right: 8px;
  262. &:last-child {
  263. margin-right: 0;
  264. }
  265. }
  266. .profile-picture {
  267. margin-right: 10px;
  268. width: 50px;
  269. height: 50px;
  270. }
  271. :deep(.profile-picture.using-initials span) {
  272. font-size: 20px; // 2/5th of .profile-picture height/width
  273. }
  274. }
  275. }
  276. </style>