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}}
-
-
+
{{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',
+ );
+ });
+ },
+);