Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

294 行
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>