Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
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.
 
 
 
 
 
 

318 lines
8.2 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>Subscriptions</h3>
  49. <label for="">Standard Plan ($49/month)</label>
  50. <button @click="unsubscribe">Unsubscribe</button>
  51. <label for="">Newsletter</label>
  52. <button @click="changeNewsSub">
  53. {{props.user.newsletter ? "Unsubscribe" : "Subscribe"}}
  54. </button>
  55. </section>
  56. <section class="form inputs special">
  57. <h3>Reset Password</h3>
  58. <label for="">Old Password</label>
  59. <input type="password" v-model="oldPass">
  60. <label for="">New Password</label>
  61. <input type="password" v-model="newPass">
  62. <label for="">Confirm Password</label>
  63. <input type="password" v-model="confirmPass">
  64. <button
  65. :disabled="!(oldPass && newPass && confirmPass)"
  66. @click="changePassword">Save</button>
  67. <label class="error">{{passError}}</label>
  68. </section>
  69. <Dialog v-if="ready" @close="() => ready = false">
  70. <h3>Confirm your current password to save changes.</h3>
  71. <input type="text">
  72. <button>Confirm</button>
  73. </Dialog>
  74. </div>
  75. </template>
  76. <script setup>
  77. import { ref, watch, onMounted } from "vue"
  78. import Dialog from "./dialog.vue"
  79. import Dropdown from "./dropdown.vue"
  80. let avatar = ref(null) // the canvas element
  81. let letterhead = ref(null) // the canvas element
  82. let ready = ref(false)
  83. let avatarChanged = ref(false)
  84. let avatarError = ref('')
  85. let letterheadError = ref('')
  86. let passError = ref('')
  87. let oldPass = ref('')
  88. let newPass = ref('')
  89. let confirmPass = ref('')
  90. const locationsId = ref(null)
  91. const addresses = ref([])
  92. const address = ref({})
  93. const props = defineProps(['user', 'token'])
  94. const emit = defineEmits(['updateAvatar', 'updateLetterhead', 'updateUser'])
  95. const user = Object.assign({}, props.user)
  96. function save() {
  97. }
  98. function check() {
  99. ready.value = true
  100. }
  101. function uploadAvatar() {
  102. avatar.value.toBlob(b => {
  103. fetch(`/api/user/avatar`,
  104. {method: 'POST',
  105. body: b,
  106. headers: {
  107. "Accept": "application/json",
  108. "Authorization": `Bearer ${props.token}`,
  109. },
  110. }).then(resp => {
  111. if (resp.ok) {emit('updateAvatar')}
  112. })
  113. })
  114. }
  115. function uploadLetterhead() {
  116. letterhead.value.toBlob(b => {
  117. fetch(`/api/user/letterhead`,
  118. {method: 'POST',
  119. body: b,
  120. headers: {
  121. "Accept": "application/json",
  122. "Authorization": `Bearer ${props.token}`,
  123. },
  124. }).then(resp => {
  125. if (resp.ok) {emit('updateLetterhead')}
  126. })
  127. })
  128. }
  129. function setLetterhead(f) {
  130. letterheadError.value = ""
  131. const validTypes = ['image/jpeg', 'image/png']
  132. if (!validTypes.includes(f.type)) {
  133. letterheadError.value = 'Image must be JPEG of PNG format'
  134. return
  135. }
  136. fetch(`/api/letterhead`,
  137. {method: 'POST',
  138. body: f,
  139. headers: {
  140. "Accept": "application/json",
  141. "Authorization": `Bearer ${props.token}`,
  142. },
  143. }).then(resp => {
  144. if (resp.ok) {
  145. resp.blob().then(b => changeLetterhead(b))
  146. } else {
  147. resp.text().then(e => letterheadError.value = e)
  148. }
  149. })
  150. }
  151. function changeAvatar(blob) {
  152. const validTypes = ['image/jpeg', 'image/png']
  153. if (!validTypes.includes(blob?.type)) {
  154. avatarError.value = 'Image must be JPEG of PNG format'
  155. return
  156. }
  157. avatarError.value = ''
  158. createImageBitmap(blob,
  159. {resizeWidth: 200, resizeHeight: 200, resizeQuality: 'medium'}).
  160. then((img) => {
  161. avatar.value.getContext("2d").drawImage(img, 0, 0, 200, 200)
  162. })
  163. }
  164. function changeLetterhead(blob) {
  165. const validTypes = ['image/jpeg', 'image/png']
  166. if (!validTypes.includes(blob.type)) {
  167. letterheadError.value = 'Image must be JPEG of PNG format'
  168. return
  169. }
  170. createImageBitmap(blob).
  171. then((img) => {
  172. let ctx = letterhead.value.getContext("2d")
  173. ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
  174. ctx.drawImage(img, 0, 0)
  175. })
  176. }
  177. function setAddress(a) {
  178. address.value = {
  179. id: address.value.id,
  180. full: a.full_address,
  181. street: a.address,
  182. city: a.context?.place.name ?? '',
  183. region: a.context?.region?.name ?? '',
  184. country: a.context?.country?.name ?? '',
  185. zip: a.context?.postcode?.name ?? '',
  186. }
  187. addresses.value = null
  188. }
  189. function saveProfile() {
  190. user.value.address = address.value
  191. fetch(`/api/user`,
  192. {method: 'PATCH',
  193. body: JSON.stringify(user.value),
  194. headers: {
  195. "Accept": "application/json",
  196. "Authorization": `Bearer ${props.token}`,
  197. },
  198. }).then(resp => {
  199. if (resp.ok) {}
  200. })
  201. }
  202. function changePassword(f) {
  203. fetch(`/api/user/password`,
  204. {method: 'POST',
  205. body: JSON.stringify({
  206. old: oldPass.value,
  207. new: newPass.value,
  208. confirm: confirmPass.value,
  209. }),
  210. headers: {
  211. "Accept": "application/json",
  212. "Authorization": `Bearer ${props.token}`,
  213. },
  214. }).then(resp => {
  215. if (resp.ok) {
  216. resp.blob().then(b => {
  217. oldPass.value = ""
  218. newPass.value = ""
  219. confirmPass.value = ""
  220. passError.value = ""
  221. })
  222. } else {
  223. resp.text().then(e => passError.value = e)
  224. }
  225. })
  226. }
  227. function searchLocation(e) {
  228. address.value.full = e.target.value
  229. clearTimeout(locationsId.value)
  230. locationsId.value = setTimeout(getLocations, 1000, e)
  231. }
  232. function getLocations(e) {
  233. let prefix = "https://api.mapbox.com/search/searchbox/v1/suggest?"
  234. let search = e.target.value
  235. let key = encodeURIComponent(process.env.MAPBOX_API_KEY)
  236. if (!search) return
  237. fetch(`${prefix}q=${search}&proximity=ip&access_token=${key}&session_token=1`
  238. ).then(resp => resp.json()).then(entries => {
  239. if (!entries?.suggestions) {
  240. addresses.value = null
  241. return
  242. }
  243. addresses.value = entries.suggestions.filter(e => e.full_address)
  244. })
  245. }
  246. function unsubscribe() {
  247. }
  248. function changeNewsSub() {
  249. fetch(`/api/user/newsletter`,
  250. {method: 'GET',
  251. headers: {
  252. "Accept": "application/json",
  253. "Authorization": `Bearer ${props.token}`,
  254. },
  255. }).then(resp => {
  256. if (resp.ok) emit('updateUser')
  257. })
  258. }
  259. watch(props.user, (u) => {
  260. if (props.user.avatar) {
  261. changeAvatar(props.user.avatar)
  262. }
  263. if (props.user.letterhead) {
  264. changeLetterhead(props.user.letterhead)
  265. }
  266. address.value = Object.assign({}, props.user.address)
  267. user.value = Object.assign({}, props.user)
  268. user.value.address = address.value
  269. }, {immediate: true})
  270. </script>
  271. <style scoped>
  272. div.address-entry {
  273. display: flex;
  274. flex-flow: column;
  275. position: relative;
  276. }
  277. </style>