You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

492 lines
18 KiB

2 years ago
  1. <template>
  2. <div class="editPersonalInfo xl:tw-max-w-[1246px] xl:tw-mx-auto">
  3. <div class="xl:tw-flex xl:tw-justify-between xl:tw-items-start">
  4. <userSidebar :userData="userData" :firstName="firstName" :lastName="lastName" class="tw-hidden xl:tw-block">
  5. </userSidebar>
  6. <!-- <div class="xl:tw-hidden"></div> -->
  7. <div class="tw-bg-white xl:tw-p-[30px] xl:tw-rounded-[20px] xl:tw-min-w-[900px] xl:tw-max-w-[900px]">
  8. <div class="tw-text-[20px] tw-font-bold tw-text-base-primary tw-mb-[20px] md:t24 md:tw-mb-[30px]">
  9. <two-dots class="tw-mr-[30px]"></two-dots>{{ $t("userProfile.editPersonalInfo") }}
  10. </div>
  11. <!-- alert -->
  12. <div v-if="successUpdate" class="tw-flex tw-w-full tw-right-auto tw-left-auto">
  13. <div type="success"
  14. class="tw-absolute tw-top-0 tw-right-0 tw-left-0 tw-mx-auto tw-flex tw-flex-row tw-w-fit tw-items-center tw-bg-success-background tw-rounded-[12px] tw-px-[12px] tw-py-[10px]">
  15. <img src="~/assets/svg/alert_success.svg" class="tw-w-[16px] tw-mr-[10px] md:tw-w-[20px]" alt="" />
  16. <div class="tw-hidden md:tw-block tw-text-success-default tw-font-bold tw-text-[16px] md:tw-mr-[16px]">
  17. Awesome!
  18. </div>
  19. <div class="tw-text-[12px] tw-text-success-default tw-mr-[9px] md:tw-text-[16px] md:tw-mr-[16px]">
  20. Successfully updated.
  21. </div>
  22. <img src="~/assets/svg/alert_close.svg" @click="successUpdate = !successUpdate" />
  23. </div>
  24. </div>
  25. <div v-if="fileTooBig" class="tw-flex tw-w-full tw-right-auto tw-left-auto">
  26. <div type="error"
  27. class="tw-absolute tw-top-0 tw-right-0 tw-left-0 tw-mx-auto tw-flex tw-flex-row tw-w-fit tw-items-center tw-bg-error-background tw-rounded-[16px] tw-px-[12px] tw-py-[10px] tw-text-[12px]">
  28. <img src="~/assets/svg/alert_notice.svg" class="tw-w-[16px] tw-mr-[10px] md:tw-w-[20px]" alt="" />
  29. <div class="tw-hidden md:tw-block tw-text-error-default tw-mr-[25px] tw-text-[16px]">
  30. Oops!
  31. </div>
  32. <div class="tw-text-error-default tw-mr-[8px] md:tw-text-[16px] md:tw-mr-[16px]">
  33. Please upload an image under 2MB.
  34. </div>
  35. <img src="~/assets/svg/alert_close_red.svg" @click="fileTooBig = !fileTooBig" alt="" />
  36. </div>
  37. </div>
  38. <!-- user picture -->
  39. <div class="tw-flex tw-items-center tw-mb-[30px]">
  40. <div>
  41. <img v-if="userData.picture" :src="userData.picture"
  42. class="tw-border tw-border-solid tw-border-neutral-300 tw-h-[64px] tw-w-[64px] tw-rounded-[50px] tw-mr-[15px] md:tw-h-[90px] md:tw-w-[90px] md:tw-mr-[30px]"
  43. alt="" />
  44. <div v-else
  45. class="tw-flex tw-justify-center tw-items-center tw-bg-primary-1 tw-text-white tw-text-[32px] tw-h-[64px] tw-w-[64px] tw-rounded-[50px] tw-mr-[15px] md:tw-h-[90px] md:tw-w-[90px] md:tw-mr-[30px]">
  46. {{ lastName.split("")[0] || "" }}
  47. </div>
  48. </div>
  49. <div>
  50. <button
  51. class="tw-border tw-border-solid tw-border-primary-1 tw-text-[14px] tw-text-primary-1 tw-px-[16px] tw-py-[9px] tw-rounded-[12px] md:tw-text-[16px]"
  52. @click="handleUploadEvent">
  53. {{ $t("userProfile.uploadPicture") }}
  54. </button>
  55. <input type="file" class="d-none" ref="uploader" @change="userPictureUpload" />
  56. </div>
  57. </div>
  58. <!-- form -->
  59. <form
  60. class="tw-grid tw-grid-cols-1 tw-gap-y-[20px] tw-mb-[58px] md:tw-grid-cols-2 md:tw-gap-x-[60px] md:tw-mb-[50px]">
  61. <!-- tilte -->
  62. <div class="element md:tw-col-span-2 md:tw-max-w-[394px]">
  63. <div class="tw-text-[14px] tw-text-base-primary tw-mb-[10px]">
  64. {{ $t("userProfile.title") }}
  65. </div>
  66. <elementSelectWithIndex :select="{
  67. id: 'title',
  68. label: 'Title',
  69. required: false,
  70. }" :yearList="genderOptions" :default="userData.title" :validation="validation.title"
  71. @change="userData.title = $event"></elementSelectWithIndex>
  72. </div>
  73. <!-- firstName -->
  74. <div class="element">
  75. <elementInput :input="{
  76. id: 'firstName',
  77. label: 'First Name',
  78. required: true,
  79. type: 'text',
  80. }" :default="userData.first_name" :validation="validation.first_name"
  81. @change="userData.first_name = $event"></elementInput>
  82. </div>
  83. <!-- lastName -->
  84. <div class="element">
  85. <elementInput :input="{
  86. id: 'lastName',
  87. label: 'Last Name',
  88. required: true,
  89. type: 'text',
  90. }" :default="userData.last_name" :validation="validation.last_name" @change="userData.last_name = $event">
  91. </elementInput>
  92. </div>
  93. <!-- email -->
  94. <div class="element">
  95. <elementInput :input="{
  96. id: 'email',
  97. label: 'userProfile.personalMail',
  98. required: true,
  99. type: 'email',
  100. }" :default="userData.email" :validation="validation.email" @change="userData.email = $event">
  101. </elementInput>
  102. </div>
  103. <!-- Phone -->
  104. <div>
  105. <div class="tw-text-[14px] tw-text-base-primary tw-mb-[10px]">
  106. {{ $t("userProfile.phone") }}
  107. </div>
  108. <div>
  109. <vue-country-code class="d-none" v-model="countryCode" enabledCountryCode :enabledFlags="false"
  110. @onSelect="showCode"></vue-country-code>
  111. <vue-phone-number-input v-model="userData.phone" color="#EF5A5A" valid-color="#EE9546" :border-radius="5"
  112. no-flags no-example @update="getPhoneData" :translations="translateOption">
  113. </vue-phone-number-input>
  114. </div>
  115. </div>
  116. <!-- Birthday -->
  117. <div class="md:tw-col-span-2 xl:tw-col-span-1">
  118. <div class="tw-text-[14px] tw-text-base-primary tw-mb-[10px]">
  119. {{ $t("userProfile.birthday")
  120. }}<span class="tw-text-error-default">*</span>
  121. </div>
  122. <div class="tw-grid tw-grid-cols-3 tw-gap-[20px]">
  123. <div class="element">
  124. <elementSelectWithIndex :select="{
  125. id: 'yearSelect',
  126. required: true,
  127. }" :yearList="yearOptions" :default="yearSelect" :validation="validation.yearSelect"
  128. @change="yearSelect = $event"></elementSelectWithIndex>
  129. </div>
  130. <div class="element">
  131. <elementSelectWithIndex :select="{
  132. id: 'monthSelect',
  133. required: false,
  134. }" :yearList="monthOptions" :default="monthSelect" :validation="validation.monthSelect"
  135. @change="monthSelect = $event"></elementSelectWithIndex>
  136. </div>
  137. <div class="element">
  138. <elementSelectWithIndex :select="{
  139. id: 'daySelect',
  140. required: false,
  141. }" :yearList="dayOptions" :default="daySelect" :validation="validation.daySelect"
  142. @change="daySelect = $event"></elementSelectWithIndex>
  143. </div>
  144. </div>
  145. </div>
  146. <!-- Country -->
  147. <div class="element md:tw-col-span-2 xl:tw-col-span-1">
  148. <elementSelect :select="{
  149. id: 'Country',
  150. label: 'userProfile.countryAndRegion',
  151. required: true,
  152. }" :selectList="countryOptions" :default="userData.country" :validation="validation.country"
  153. @change="userData.country = $event"></elementSelect>
  154. </div>
  155. <!-- language -->
  156. <v-spacer class="tw-items-center md:tw-flex md:tw-justify-start md:tw-mt-[20px] md:tw-flex-nowrap">
  157. <div class="tw-inline-block tw-mb-[20px] md:tw-whitespace-nowrap md:tw-mb-0">
  158. {{ $t("userProfile.preferLanguage") }}
  159. </div>
  160. <v-spacer class="tw-flex md:tw-w-fit md:tw-ml-[30px]">
  161. <v-checkbox v-model="languageSelect.en" class="mt-0" hide-details :label="$t('English')"></v-checkbox>
  162. <v-checkbox v-model="languageSelect.zhtw" class="ms-10 mt-0" hide-details :label="$t('Chinese')">
  163. </v-checkbox>
  164. </v-spacer>
  165. </v-spacer>
  166. </form>
  167. <!-- button -->
  168. <div>
  169. <button
  170. class="tw-text-[18px] tw-bg-primary-1 tw-text-white tw-py-[12px] tw-w-full tw-rounded-[16px] tw-mb-[10px] md:tw-w-fit md:tw-px-[16px] md:tw-mr-[20px]"
  171. @click="patchUserData()">
  172. {{ $t("userProfile.saveButton") }}
  173. </button>
  174. <button
  175. class="tw-text-[18px] tw-bg-white tw-text-primary-1 tw-py-[12px] tw-w-full tw-rounded-[16px] md:tw-w-fit md:tw-px-[16px] xl:tw-hidden">
  176. {{ $t("Cancel") }}
  177. </button>
  178. </div>
  179. </div>
  180. </div>
  181. <CropImageDialog :isCropImageDialogActive="isCropImageDialogActive" :cropImagePreview="cropImagePreview"
  182. @close-crop-dialog="closeCropDialog" @upload-image-success="handleImageUpdate"></CropImageDialog>
  183. </div>
  184. </template>
  185. <script>
  186. import elementSelect from "@/components/newComponent/form/ElementSelect.vue";
  187. import elementInput from "@/components/newComponent/form/ElementInput.vue";
  188. import SavedExhibitions from "../../components/user/savedExhibitions.vue";
  189. import SavedExhibitionsDialog from "../../components/user/savedExhibitionsDialog.vue";
  190. import CropImageDialog from "../../components/user/cropImageDialog.vue";
  191. import TwoDots from "@/components/TwoDots";
  192. import userSidebar from "@/components/user/userSidebar.vue";
  193. import elementSelectWithIndex from "@/components/newComponent/form/ElementSelectWithIndex.vue";
  194. import is from "is_js";
  195. export default {
  196. name: "editPersonalInfo",
  197. layout: "profile",
  198. components: {
  199. elementSelect,
  200. elementInput,
  201. SavedExhibitions,
  202. SavedExhibitionsDialog,
  203. CropImageDialog,
  204. TwoDots,
  205. userSidebar,
  206. elementSelectWithIndex,
  207. is,
  208. },
  209. data() {
  210. return {
  211. genderOptions: ["Mr.", "Ms."],
  212. successUpdate: false,
  213. fileTooBig: false,
  214. firstName: "",
  215. lastName: "",
  216. countryCode: "",
  217. userData: {},
  218. userCompanyId: [],
  219. propUserCompany: {},
  220. yearOptions: [],
  221. monthOptions: [],
  222. dayOptions: [],
  223. countryOptions: [],
  224. countrySortOptions: [],
  225. yearSelect: "",
  226. monthSelect: "",
  227. daySelect: "",
  228. languageSelect: {
  229. en: "",
  230. zhtw: "",
  231. },
  232. isCropImageDialogActive: false,
  233. cropImagePreview: "",
  234. dateMenu: false,
  235. translateOption: {
  236. countrySelectorLabel: this.$t("country code"),
  237. phoneNumberLabel: this.$t("phone number"),
  238. },
  239. rules: {
  240. require: (value) => !!value || this.$t("Required."),
  241. email: (v) =>
  242. /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/.test(
  243. v
  244. ) || this.$t("Invalid email"),
  245. checkPassword: (v) =>
  246. (/(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])/.test(v) &&
  247. v.length >= 8 &&
  248. v.length <= 20) ||
  249. this.$t(
  250. "Passwords must be 8-20 characters with at least 1 number, 1 lower case letter and 1 upper case letter"
  251. ),
  252. },
  253. personalInfoValid: false,
  254. phoneValid: false,
  255. isNavActive: true,
  256. // width: 0,
  257. validation: {
  258. title: true,
  259. first_name: true,
  260. last_name: true,
  261. email: true,
  262. yearSelect: true,
  263. monthSelect: true,
  264. daySelect: true,
  265. country: true,
  266. },
  267. errors: null,
  268. };
  269. },
  270. created() {
  271. //this.fetchCountry();
  272. this.$store.dispatch("updatePicture");
  273. //this.fetchUserData();
  274. },
  275. mounted() {
  276. this.yearOptions = Array.from(new Array(103), (val, index) =>
  277. (index + 1920).toString()
  278. );
  279. this.monthOptions = Array.from(new Array(13), (val, index) => {
  280. if (index < 10 && index > 0) {
  281. return "0" + index.toString();
  282. }
  283. if (index >= 10) {
  284. return index.toString();
  285. }
  286. });
  287. this.dayOptions = Array.from(new Array(32), (val, index) => {
  288. if (index < 10 && index > 0) {
  289. return "0" + index.toString();
  290. }
  291. if (index >= 10) {
  292. return index.toString();
  293. }
  294. });
  295. this.$nextTick(() => {
  296. window.addEventListener("resize", this.onResize);
  297. });
  298. },
  299. methods: {
  300. patchUserData() {
  301. // if (this.width < 1366) {
  302. // this.isEditInfoDialogActive = !this.isEditInfoDialogActive;
  303. // }
  304. this.validators();
  305. if (this.validators()) {
  306. this.userData.prefer_country = JSON.stringify(this.languageSelect);
  307. if (this.$vuetify.breakpoint.name !== "xs") {
  308. this.userData.birth_date =
  309. this.yearSelect + "-" + this.monthSelect + "-" + this.daySelect;
  310. if (this.userData.birth_date.length < 10) {
  311. this.userData.birth_date = null;
  312. }
  313. }
  314. const patchData = JSON.parse(JSON.stringify(this.userData));
  315. delete patchData.LoginLog;
  316. delete patchData.UserCompany;
  317. delete patchData.UserSocialRelation;
  318. delete patchData.UserExhibition;
  319. this.$axios
  320. .put(
  321. `/member/users/${this.$auth.$storage.getUniversal("jwt").user_id
  322. }?jwt=${this.$auth.$storage.getUniversal("jwt").token || ""
  323. }`,
  324. patchData
  325. )
  326. .then((res) => {
  327. if (res.status == 200) {
  328. this.$notify({
  329. type: "success",
  330. text: "更新成功",
  331. });
  332. }
  333. this.fetchUserData();
  334. this.$auth.$storage.setUniversal("userPicture", patchData.picture);
  335. this.$auth.$storage.setUniversal(
  336. "userLastName",
  337. patchData.last_name
  338. );
  339. this.$store.dispatch("updatePicture");
  340. })
  341. .catch((err) => {
  342. console.log(err);
  343. });
  344. }
  345. },
  346. fetchUserData() {
  347. this.$axios
  348. .get(
  349. `/member/users/${this.$auth.$storage.getUniversal("jwt").user_id
  350. }?jwt=${this.$auth.$storage.getUniversal("jwt").token}`
  351. )
  352. .then((res) => {
  353. this.userData = res.data;
  354. this.userCompanyId = res.data.UserCompany;
  355. this.firstName = res.data.first_name;
  356. this.lastName = res.data.last_name;
  357. this.userData.phone
  358. ? (this.phoneValid = true)
  359. : (this.phoneValid = false);
  360. !this.userData.prefer_country &&
  361. typeof this.userData.prefer_country === "object"
  362. ? this.userData.prefer_country
  363. : (this.languageSelect = JSON.parse(this.userData.prefer_country));
  364. if (
  365. !this.userData.birth_date &&
  366. typeof this.userData.birth_date === "object"
  367. ) {
  368. this.yearSelect = "";
  369. this.monthSelect = "";
  370. this.daySelect = "";
  371. } else {
  372. const date = this.userData.birth_date.split("-");
  373. this.yearSelect = date[0];
  374. this.monthSelect = date[1];
  375. this.daySelect = date[2];
  376. }
  377. })
  378. .catch((err) => {
  379. console.log(err);
  380. });
  381. },
  382. handleUploadEvent() {
  383. window.addEventListener("focus", () => { }, { once: true });
  384. this.$refs.uploader.click();
  385. },
  386. userPictureUpload(e) {
  387. const payload = new FormData();
  388. payload.append("file", e.target.files[0]);
  389. const fileSize = (e.target.files[0].size / 1024 / 1024).toFixed(1);
  390. if (fileSize >= 2) {
  391. this.fileTooBig = !this.fileTooBig;
  392. } else {
  393. this.$axios
  394. .post("/users/images", payload)
  395. .then((response) => {
  396. this.cropImagePreview = response.data.image.url;
  397. })
  398. .catch((error) => console.log(error));
  399. this.isCropImageDialogActive = !this.isCropImageDialogActive;
  400. }
  401. },
  402. getPhoneData(phoneData) {
  403. this.countryCode = phoneData.countryCode;
  404. this.phoneValid = phoneData.isValid;
  405. },
  406. showCode(object) {
  407. this.userData.country_code = object.dialCode;
  408. },
  409. fetchCountry() {
  410. this.$axios
  411. .get(`/users/countries?lang=${this.$i18n.locale.replace("-", "")}`)
  412. .then((res) => {
  413. this.countryOptions = res.data.result;
  414. })
  415. .catch((err) => {
  416. console.log(err);
  417. });
  418. },
  419. closeCropDialog() {
  420. this.isCropImageDialogActive = !this.isCropImageDialogActive;
  421. },
  422. handleImageUpdate(pictureURL) {
  423. this.userData.picture = pictureURL;
  424. this.patchUserData();
  425. this.closeCropDialog();
  426. },
  427. validators() {
  428. if (is.empty(this.userData.first_name)) {
  429. this.validation.first_name = false;
  430. } else {
  431. this.validation.first_name = true;
  432. }
  433. if (is.empty(this.userData.last_name)) {
  434. this.validation.last_name = false;
  435. } else {
  436. this.validation.last_name = true;
  437. }
  438. if (is.empty(this.userData.email) || is.not.email(this.userData.email)) {
  439. this.validation.email = false;
  440. } else {
  441. this.validation.email = true;
  442. }
  443. if (is.empty(this.yearSelect)) {
  444. this.validation.yearSelect = false;
  445. } else {
  446. this.validation.yearSelect = true;
  447. }
  448. if (is.empty(this.monthSelect)) {
  449. this.validation.monthSelect = false;
  450. } else {
  451. this.validation.monthSelect = true;
  452. }
  453. if (is.empty(this.daySelect)) {
  454. this.validation.daySelect = false;
  455. } else {
  456. this.validation.daySelect = true;
  457. }
  458. if (is.null(this.userData.country) || this.userData.country == 0) {
  459. this.validation.country = false;
  460. } else {
  461. this.validation.country = true;
  462. }
  463. this.errors = Object.entries(this.validation).filter(
  464. (e) => e[1] == false
  465. );
  466. if (this.errors.length > 0) {
  467. return false;
  468. } else {
  469. return true;
  470. }
  471. },
  472. },
  473. };
  474. </script>
  475. <style scoped lang="scss" scoped>
  476. .success-alert {
  477. position: absolute;
  478. left: 13%;
  479. z-index: 10;
  480. top: -3%;
  481. }
  482. .success-alert-dialog {
  483. position: absolute;
  484. left: 13%;
  485. z-index: 10;
  486. top: 3%;
  487. }
  488. </style>