feat(ui): rebuild all tools pages on Carbon Design System web components

- @carbon/web-components v2.50.0 via the IBM CDN (version-pinned);
  @carbon/styles CSS + IBM Plex Sans for tokens/typography
- landing: Carbon header + cds-tile tool cards
- fingerprint wizard: Carbon-styled form controls, cds-progress-bar scan
  progress, cds-tag badges, cds-inline-notification replacing swal
- cropper: cds-toast replacing toastr, cds-checkbox review reasons,
  cds-tag process badge; vue-croppa pad kept in the Carbon layout
- Bootstrap 4, swal, toastr and jQuery removed from all pages
- behavior frozen: same flows, endpoints, validation and copy
This commit is contained in:
2026-10-01 02:03:07 +01:00
parent 22d9ba40e3
commit 930af97f05
12 changed files with 522 additions and 343 deletions
+62 -50
View File
@@ -1,32 +1,13 @@
/* Image Cropper — review, crop, clean and save identity photos.
Identity comes from the YOUreka session; the only operator credential is
the save-time OTP (required by a2loft). */
/* global Vue, axios, toastr, Croppa */
Vue.use(Croppa);
the save-time OTP (required by a2loft). Notifications are Carbon toasts. */
/* global Vue, axios, Croppa, customElements */
Vue.config.ignoredElements = [/^cds-/];
(function () {
'use strict';
var REVIEW_OPTIONS = [
'IMAGE: Not in the midline of the photo frame (aligned to the left or right)',
'IMAGE: Taken too close-up (Only head is visible, shoulders barely visible)',
'IMAGE: Blurry ',
'IMAGE: Improper lighting ',
'IMAGE: Not coloured.',
'IMAGE: Scanned photo',
'IMAGE: Has no adequate space to allow for proper cropping ',
'IMAGE: Not taken on a recommended background.',
'IMAGE: Photo is a selfie',
'DRESSING: Unacceptable dress. ',
'HEAD: Skewed (leaning away from the midline - vertically or horizontally)',
'EYES: Not looking into the camera (looking downward, upward, or sideward)',
'EYES: Not properly opened',
'EARS: Not visible ',
'EARS: Both ears are not equally visible (face not aligned central)',
'FACIAL EXPRESSION: NOT neutral (subject has a facial expression)',
'GLASSES: Subject is wearing glasses ',
'JUST WRING: Goes against almost all acceptable passport criteria.',
'Others not specified.',
];
var toastSeq = 0;
var app = new Vue({
el: '#app',
@@ -43,12 +24,31 @@ Vue.use(Croppa);
cleanedImage: null,
requiresCleaning: '',
otp: '',
reviewOptions: REVIEW_OPTIONS.map(function (name, index) {
return { index: index + 1, name: name };
}),
reviewOptions: [
'IMAGE: Not in the midline of the photo frame (aligned to the left or right)',
'IMAGE: Taken too close-up (Only head is visible, shoulders barely visible)',
'IMAGE: Blurry ',
'IMAGE: Improper lighting ',
'IMAGE: Not coloured.',
'IMAGE: Scanned photo',
'IMAGE: Has no adequate space to allow for proper cropping ',
'IMAGE: Not taken on a recommended background.',
'IMAGE: Photo is a selfie',
'DRESSING: Unacceptable dress. ',
'HEAD: Skewed (leaning away from the midline - vertically or horizontally)',
'EYES: Not looking into the camera (looking downward, upward, or sideward)',
'EYES: Not properly opened',
'EARS: Not visible ',
'EARS: Both ears are not equally visible (face not aligned central)',
'FACIAL EXPRESSION: NOT neutral (subject has a facial expression)',
'GLASSES: Subject is wearing glasses ',
'JUST WRING: Goes against almost all acceptable passport criteria.',
'Others not specified.',
].map(function (name, index) { return { index: index + 1, name: name }; }),
selectedReviewOptions: [],
otherReviewOption: '',
busy: { load: false, clean: false, review: false, save: false },
toasts: [],
myCroppa: {},
},
@@ -78,11 +78,31 @@ Vue.use(Croppa);
return;
}
self.loading = false;
toastr.error('Could not load your session. Please refresh the page.', 'Oops!');
self.toast('error', 'Session error', 'Could not load your session. Please refresh the page.');
});
Promise.all(
['cds-button', 'cds-toast', 'cds-toast-item', 'cds-tag'].map(function (n) {
return customElements.whenDefined(n).catch(function () {});
})
).then(function () {
self.$forceUpdate();
});
},
methods: {
toast: function (kind, title, caption) {
var self = this;
var id = ++toastSeq;
this.toasts.push({ id: id, kind: kind, title: title, caption: caption || '' });
setTimeout(function () {
self.removeToast(id);
}, 6000);
},
removeToast: function (id) {
this.toasts = this.toasts.filter(function (t) { return t.id !== id; });
},
reset: function () {
this.message = { message: { caption: null, file_url: null, domain: '' } };
this.agent = null;
@@ -110,7 +130,7 @@ Vue.use(Croppa);
);
if (!data.ok) {
this.reset(); // nothing (more) to review — back to the load state
toastr.error(data.message, 'Oops!');
this.toast('info', 'No picture', data.message || 'Nothing to load.');
return;
}
this.message = data.data;
@@ -119,7 +139,7 @@ Vue.use(Croppa);
);
if (!image.data.ok) {
this.reset();
toastr.error("Couldn't load the attached image. Please try again.", 'Oops!');
this.toast('error', 'Load failed', "Couldn't load the attached image. Please try again.");
return;
}
this.message.message.file_url = image.data.data;
@@ -133,14 +153,8 @@ Vue.use(Croppa);
this.agent = review.data.ok ? review.data.data : null;
}
} catch ({ message }) {
toastr.error(message, 'Operation Failed');
this.toast('error', 'Operation Failed', message);
} finally {
this.reviewed = false;
this.accepted = false;
this.selectedReviewOptions = [];
this.otherReviewOption = '';
this.requiresCleaning = '';
this.otp = '';
this.busy.load = false;
}
},
@@ -168,7 +182,7 @@ Vue.use(Croppa);
fr.onload = function () { self.cleanedImage = fr.result; };
fr.readAsDataURL(blob);
} catch ({ message }) {
toastr.error(message, 'Image Cleanup Failed');
this.toast('error', 'Image Cleanup Failed', message);
} finally {
this.busy.clean = false;
}
@@ -178,7 +192,7 @@ Vue.use(Croppa);
var self = this;
if (!this.canReview) return;
if (!this.accepted && this.selectedReviewOptions.length === 0 && !this.otherReviewOption) {
toastr.error('Please indicate why this image is not good enough.', 'Operation Failed');
this.toast('error', 'Operation Failed', 'Please indicate why this image is not good enough.');
return;
}
this.busy.review = true;
@@ -206,15 +220,15 @@ Vue.use(Croppa);
...data,
});
if (!result.ok) {
toastr.error(result.message, 'Operation Failed');
this.toast('error', 'Operation Failed', result.message || 'Review failed.');
return;
}
if (reviewMessage) {
toastr.success('Profile owner has been notified of the review.', 'Review Sent');
this.toast('success', 'Review Sent', 'Profile owner has been notified of the review.');
}
this.reset();
} catch ({ message }) {
toastr.error(message, 'Operation Failed');
this.toast('error', 'Operation Failed', message);
} finally {
this.busy.review = false;
}
@@ -227,21 +241,19 @@ Vue.use(Croppa);
try {
var { data } = await axios.post('/cropper/dequeue', { message: this.message });
if (!data.ok) {
toastr.error(data.message, 'Not Removed');
this.toast('error', 'Not Removed', data.message || 'Removal failed.');
return;
}
toastr.success(data.message, 'Removed!');
this.toast('success', 'Removed!', data.message || '');
this.reset();
} catch ({ message }) {
toastr.error(message, 'Not Removed');
this.toast('error', 'Not Removed', message);
} finally {
this.busy.review = false;
}
},
saveChanges: async function () {
var self = this;
if (!this.canSave) return;
this.busy.save = true;
try {
var { data } = await axios.post(
@@ -254,13 +266,13 @@ Vue.use(Croppa);
}
);
if (!data.ok) {
toastr.error(data.message, 'Not Saved');
this.toast('error', 'Not Saved', data.message || 'Save failed.');
return;
}
toastr.success('Picture saved for ' + this.message.message.caption, 'Saved');
this.toast('success', 'Saved', 'Picture saved for ' + this.message.message.caption);
this.reset();
} catch ({ message }) {
toastr.error(message, 'Not Saved');
this.toast('error', 'Not Saved', message);
} finally {
this.busy.save = false;
}