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.

579 lines
21 KiB

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