diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs index 4bb1be77..8625b37a 100644 --- a/app/components/new-join-steps/new-step-one.hbs +++ b/app/components/new-join-steps/new-step-one.hbs @@ -2,8 +2,7 @@

Profile Picture

{{#if this.isImageUploading}} -
- +

Processing image...

{{else}} @@ -13,6 +12,7 @@ src={{this.imagePreview}} alt="Profile preview" class="image-preview" + data-test-image-preview /> { - const base64String = e.target.result; - this.imagePreview = base64String; - this.updateFieldValue?.('imageUrl', base64String); - this.isImageUploading = false; - }; - reader.onerror = () => { + try { + const formData = new FormData(); + formData.append('type', 'application'); + formData.append('profile', file); + + const response = await fetch(USER_PROFILE_IMAGE_URL, { + method: 'POST', + credentials: 'include', + body: formData, + }); + if (response.ok) { + const data = await response.json(); + const imageUrl = data?.image?.url || data.picture; + + if (!imageUrl) { + this.toast.error( + 'Upload succeeded but no image URL was returned. Please try again.', + 'Error!', + ); + return; + } + this.imagePreview = imageUrl; + this.updateFieldValue?.('imageUrl', imageUrl); + + this.toast.success( + 'Profile image uploaded successfully!', + 'Success!', + TOAST_OPTIONS, + ); + } else { + const errorData = await response.json(); + this.toast.error( + errorData.message || 'Failed to upload image. Please try again.', + 'Error!', + TOAST_OPTIONS, + ); + } + } catch (error) { this.toast.error( - 'Failed to read the selected file. Please try again.', + error.message || 'Failed to upload image. Please try again.', 'Error!', ); + } finally { this.isImageUploading = false; - }; - - reader.readAsDataURL(file); + } } } diff --git a/app/constants/apis.js b/app/constants/apis.js index 668ed29a..281bd00d 100644 --- a/app/constants/apis.js +++ b/app/constants/apis.js @@ -66,3 +66,5 @@ export const NUDGE_APPLICATION_URL = (applicationId) => { export const APPLICATIONS_BY_USER_URL = (userId) => { return `${APPS.API_BACKEND}/applications?userId=${userId}&dev=true`; }; + +export const USER_PROFILE_IMAGE_URL = `${APPS.API_BACKEND}/users/picture`; diff --git a/app/styles/new-stepper.module.css b/app/styles/new-stepper.module.css index 53748129..8e9b22aa 100644 --- a/app/styles/new-stepper.module.css +++ b/app/styles/new-stepper.module.css @@ -239,6 +239,14 @@ gap: 1rem; } +.image-preview-loading { + display: flex; + height: 8rem; + flex-direction: column; + justify-content: center; + align-items: center; +} + .image-preview { width: 9.5rem; height: 9.5rem; diff --git a/tests/integration/components/new-join-steps/new-step-one-test.js b/tests/integration/components/new-join-steps/new-step-one-test.js new file mode 100644 index 00000000..ffc65bd7 --- /dev/null +++ b/tests/integration/components/new-join-steps/new-step-one-test.js @@ -0,0 +1,141 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render, triggerEvent, waitFor } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; + +module( + 'Integration | Component | new-join-steps/new-step-one', + function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + localStorage.removeItem('newStepOneData'); + + this.toast = this.owner.lookup('service:toast'); + sinon.stub(this.toast, 'success'); + sinon.stub(this.toast, 'error'); + + this.setIsPreValid = sinon.stub(); + this.setIsValid = sinon.stub(); + }); + + hooks.afterEach(function () { + sinon.restore(); + }); + + test('handleImageSelect rejects non-image files', async function (assert) { + await render( + hbs``, + ); + + const file = new File(['pdf content'], 'document.pdf', { + type: 'application/pdf', + }); + + await triggerEvent('input[type="file"]', 'change', { + files: [file], + }); + + assert.ok( + this.toast.error.calledWithExactly( + 'Invalid file type. Please upload an image file.', + 'Error!', + ), + 'Shows error for non-image file', + ); + }); + + test('handleImageSelect rejects files larger than 2MB', async function (assert) { + await render( + hbs``, + ); + + const largeFile = new File(['x'.repeat(3 * 1024 * 1024)], 'large.jpg', { + type: 'image/jpeg', + }); + + await triggerEvent('input[type="file"]', 'change', { + files: [largeFile], + }); + + assert.ok( + this.toast.error.calledWithExactly( + 'Image size must be less than 2MB', + 'Error!', + ), + 'Shows error for oversized file', + ); + }); + + test('imagePreview and imageUrl are updated on successful upload', async function (assert) { + sinon.stub(window, 'fetch').resolves({ + ok: true, + json: async () => ({ + image: { + url: 'https://example.com/photo.jpg', + }, + }), + }); + + await render( + hbs``, + ); + + const file = new File(['image'], 'photo.jpg', { type: 'image/jpeg' }); + + await triggerEvent('input[type="file"]', 'change', { + files: [file], + }); + + await waitFor('[data-test-image-preview]'); + + assert.dom('[data-test-image-preview]').exists(); + assert.dom('[data-test-image-preview]').hasAttribute('src'); + + const storedData = JSON.parse( + localStorage.getItem('newStepOneData') || '{}', + ); + assert.strictEqual( + storedData.imageUrl, + 'https://example.com/photo.jpg', + 'Persists returned image URL to localStorage', + ); + assert.ok( + this.toast.success.calledWithExactly( + 'Profile image uploaded successfully!', + 'Success!', + sinon.match.object, + ), + 'Shows success toast', + ); + }); + + test('shows error toast on image upload API failure', async function (assert) { + sinon.stub(window, 'fetch').resolves({ + ok: false, + json: async () => ({ message: 'Server error' }), + }); + + await render( + hbs``, + ); + + const file = new File(['image'], 'photo.jpg', { type: 'image/jpeg' }); + + await triggerEvent('input[type="file"]', 'change', { + files: [file], + }); + + assert.dom('[data-test-image-preview]').doesNotExist(); + assert.ok( + this.toast.error.calledWithExactly( + 'Server error', + 'Error!', + sinon.match.object, + ), + 'Shows error toast with API message', + ); + }); + }, +);