Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

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>