Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 
 

294 rindas
7.6 KiB

  1. <template>
  2. <div class="page settings">
  3. <h2>Settings</h2>
  4. <section class="form inputs">
  5. <h3>Avatar</h3>
  6. <canvas class="displayer" width="200" height="200" ref="avatar"></canvas>
  7. <input type="file"
  8. @change="e => changeAvatar(e.target.files[0])"
  9. />
  10. <button @click="uploadAvatar">Upload</button>
  11. <label class="error">{{avatarError}}</label>
  12. </section>
  13. <section class="form inputs letterhead">
  14. <h3>Letterhead</h3>
  15. <canvas class="displayer" height="200" ref="letterhead"></canvas>
  16. <input type="file"
  17. @change="e => {setLetterhead(e.target.files[0])}"
  18. />
  19. <button @click="uploadLetterhead">Upload</button>
  20. <label class="error">{{letterheadError}}</label>
  21. </section>
  22. <section class="form inputs special">
  23. <h3>Profile</h3>
  24. <label for="">First Name</label>
  25. <input type="text" v-model="user.firstName">
  26. <label for="">Last Name</label>
  27. <input type="text" :value="user.lastName">
  28. <label for="">NMLS ID</label>
  29. <input type="text">
  30. <label for="">Branch ID</label>
  31. <input type="text" :value="user.branchId" disabled>
  32. <select id="" name="" :value="user.country">
  33. <option value="USA">USA</option>
  34. <option value="Canada">Canada</option>
  35. </select>
  36. <div class="address-entry">
  37. <label for="">Address</label>
  38. <input type="text" @input="searchLocation" :value="address.full">
  39. <dropdown v-if="addresses && addresses.length"
  40. :entries="addresses.map(a => ({text: a.full_address, value: a}))"
  41. @select="setAddress"
  42. >
  43. </dropdown>
  44. </div>
  45. <button @click="saveProfile">Save</button>
  46. </section>
  47. <section class="form inputs special">
  48. <h3>Reset Password</h3>
  49. <label for="">Old Password</label>
  50. <input type="password" v-model="oldPass">
  51. <label for="">New Password</label>
  52. <input type="password" v-model="newPass">
  53. <label for="">Confirm Password</label>
  54. <input type="password" v-model="confirmPass">
  55. <button
  56. :disabled="!(oldPass && newPass && confirmPass)"
  57. @click="changePassword">Save</button>
  58. <label class="error">{{passError}}</label>
  59. </section>
  60. <Dialog v-if="ready" @close="() => ready = false">
  61. <h3>Confirm your current password to save changes.</h3>
  62. <input type="text">
  63. <button>Confirm</button>
  64. </Dialog>
  65. </div>
  66. </template>
  67. <script setup>
  68. import { ref, watch, onMounted } from "vue"
  69. import Dialog from "./dialog.vue"
  70. import Dropdown from "./dropdown.vue"
  71. let avatar = ref(null) // the canvas element
  72. let letterhead = ref(null) // the canvas element
  73. let ready = ref(false)
  74. let avatarChanged = ref(false)
  75. let avatarError = ref('')
  76. let letterheadError = ref('')
  77. let passError = ref('')
  78. let oldPass = ref('')
  79. let newPass = ref('')
  80. let confirmPass = ref('')
  81. const locationsId = ref(null)
  82. const addresses = ref([])
  83. const address = ref({})
  84. const props = defineProps(['user', 'token'])
  85. const emit = defineEmits(['updateAvatar', 'updateLetterhead'])
  86. const user = Object.assign({}, props.user)
  87. function save() {
  88. }
  89. function check() {
  90. ready.value = true
  91. }
  92. function uploadAvatar() {
  93. avatar.value.toBlob(b => {
  94. fetch(`/api/user/avatar`,
  95. {method: 'POST',
  96. body: b,
  97. headers: {
  98. "Accept": "application/json",
  99. "Authorization": `Bearer ${props.token}`,
  100. },
  101. }).then(resp => {
  102. if (resp.ok) {emit('updateAvatar')}
  103. })
  104. })
  105. }
  106. function uploadLetterhead() {
  107. letterhead.value.toBlob(b => {
  108. fetch(`/api/user/letterhead`,
  109. {method: 'POST',
  110. body: b,
  111. headers: {
  112. "Accept": "application/json",
  113. "Authorization": `Bearer ${props.token}`,
  114. },
  115. }).then(resp => {
  116. if (resp.ok) {emit('updateLetterhead')}
  117. })
  118. })
  119. }
  120. function setLetterhead(f) {
  121. letterheadError.value = ""
  122. const validTypes = ['image/jpeg', 'image/png']
  123. if (!validTypes.includes(f.type)) {
  124. letterheadError.value = 'Image must be JPEG of PNG format'
  125. return
  126. }
  127. fetch(`/api/letterhead`,
  128. {method: 'POST',
  129. body: f,
  130. headers: {
  131. "Accept": "application/json",
  132. "Authorization": `Bearer ${props.token}`,
  133. },
  134. }).then(resp => {
  135. if (resp.ok) {
  136. resp.blob().then(b => changeLetterhead(b))
  137. } else {
  138. resp.text().then(e => letterheadError.value = e)
  139. }
  140. })
  141. }
  142. function changeAvatar(blob) {
  143. const validTypes = ['image/jpeg', 'image/png']
  144. if (!validTypes.includes(blob?.type)) {
  145. avatarError.value = 'Image must be JPEG of PNG format'
  146. return
  147. }
  148. avatarError.value = ''
  149. createImageBitmap(blob,
  150. {resizeWidth: 200, resizeHeight: 200, resizeQuality: 'medium'}).
  151. then((img) => {
  152. avatar.value.getContext("2d").drawImage(img, 0, 0, 200, 200)
  153. })
  154. }
  155. function changeLetterhead(blob) {
  156. const validTypes = ['image/jpeg', 'image/png']
  157. if (!validTypes.includes(blob.type)) {
  158. letterheadError.value = 'Image must be JPEG of PNG format'
  159. return
  160. }
  161. createImageBitmap(blob).
  162. then((img) => {
  163. let ctx = letterhead.value.getContext("2d")
  164. ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
  165. ctx.drawImage(img, 0, 0)
  166. })
  167. }
  168. function setAddress(a) {
  169. address.value = {
  170. id: address.value.id,
  171. full: a.full_address,
  172. street: a.address,
  173. city: a.context?.place.name ?? '',
  174. region: a.context?.region?.name ?? '',
  175. country: a.context?.country?.name ?? '',
  176. zip: a.context?.postcode?.name ?? '',
  177. }
  178. addresses.value = null
  179. }
  180. function saveProfile() {
  181. user.value.address = address.value
  182. fetch(`/api/user`,
  183. {method: 'PATCH',
  184. body: JSON.stringify(user.value),
  185. headers: {
  186. "Accept": "application/json",
  187. "Authorization": `Bearer ${props.token}`,
  188. },
  189. }).then(resp => {
  190. if (resp.ok) {}
  191. })
  192. }
  193. function changePassword(f) {
  194. fetch(`/api/user/password`,
  195. {method: 'POST',
  196. body: JSON.stringify({
  197. old: oldPass.value,
  198. new: newPass.value,
  199. confirm: confirmPass.value,
  200. }),
  201. headers: {
  202. "Accept": "application/json",
  203. "Authorization": `Bearer ${props.token}`,
  204. },
  205. }).then(resp => {
  206. if (resp.ok) {
  207. resp.blob().then(b => {
  208. oldPass.value = ""
  209. newPass.value = ""
  210. confirmPass.value = ""
  211. passError.value = ""
  212. })
  213. } else {
  214. resp.text().then(e => passError.value = e)
  215. }
  216. })
  217. }
  218. function searchLocation(e) {
  219. address.value.full = e.target.value
  220. clearTimeout(locationsId.value)
  221. locationsId.value = setTimeout(getLocations, 1000, e)
  222. }
  223. function getLocations(e) {
  224. let prefix = "https://api.mapbox.com/search/searchbox/v1/suggest?"
  225. let search = e.target.value
  226. let key = encodeURIComponent(process.env.MAPBOX_API_KEY)
  227. if (!search) return
  228. fetch(`${prefix}q=${search}&proximity=ip&access_token=${key}&session_token=1`
  229. ).then(resp => resp.json()).then(entries => {
  230. if (!entries?.suggestions) {
  231. addresses.value = null
  232. return
  233. }
  234. addresses.value = entries.suggestions.filter(e => e.full_address)
  235. })
  236. }
  237. watch(props.user, (u) => {
  238. if (props.user.avatar) {
  239. changeAvatar(props.user.avatar)
  240. }
  241. if (props.user.letterhead) {
  242. changeLetterhead(props.user.letterhead)
  243. }
  244. address.value = Object.assign({}, props.user.address)
  245. user.value = Object.assign({}, props.user)
  246. user.value.address = address.value
  247. }, {immediate: true})
  248. </script>
  249. <style scoped>
  250. div.address-entry {
  251. display: flex;
  252. flex-flow: column;
  253. position: relative;
  254. }
  255. </style>