assets_js_controllers_member-mobile-card-profile-controller.js
import MobileControllerBase from "./mobile-controller-base.js";
/**
* MemberMobileCardProfile Stimulus Controller
*
* Manages mobile-optimized member profile card display with PWA integration.
* Extends MobileControllerBase for centralized connection handling.
*
* Features:
* - Mobile-optimized profile card layout
* - PWA readiness integration
* - Fetch with retry for reliability
* - Dynamic card generation for plugin sections
*/
class MemberMobileCardProfile extends MobileControllerBase {
static targets = [
"cardSet",
"name",
"scaName",
"branchName",
"membershipInfo",
"backgroundCheck",
"lastUpdate",
"loading",
"memberDetails",
"profilePhotoContainer",
"profilePhoto",
"zoomPhoto",
"photoManageButton",
"photoUploadModal",
];
static values = {
url: String,
pwaReady: Boolean
}
initialize() {
super.initialize();
this.currentCard = null;
this.cardCount = 0;
this.handlePwaReady = this.handlePwaReady.bind(this);
}
/**
* Called after base class connect
*/
onConnect() {
this.element.addEventListener('pwa-ready', this.handlePwaReady);
this.updatePhotoActionsForConnection(this.online);
// Check if PWA is already ready
if (this.pwaReadyValue) {
this.loadCard();
}
}
/**
* Called after base class disconnect
*/
onDisconnect() {
this.element.removeEventListener('pwa-ready', this.handlePwaReady);
}
/**
* Handle global online/offline state changes from MobileControllerBase.
*
* @param {boolean} isOnline Current connection state.
*/
onConnectionStateChanged(isOnline) {
this.updatePhotoActionsForConnection(isOnline);
}
/**
* Handle PWA ready event
*/
handlePwaReady() {
this.pwaReadyValue = true;
}
/**
* Create a new mobile card section
*/
startCard(title) {
this.cardCount++;
const card = document.createElement("div");
card.classList.add("card", "cardbox", "m-3");
card.id = "card_" + this.cardCount;
card.dataset.section = "auth-card";
const cardDetails = document.createElement("div");
cardDetails.classList.add("card-body");
cardDetails.id = "cardDetails_" + this.cardCount;
const cardTitle = document.createElement("h3");
cardTitle.classList.add("card-title", "text-center", "display-6");
cardTitle.textContent = title;
cardDetails.appendChild(cardTitle);
card.appendChild(cardDetails);
this.cardSetTarget.appendChild(card);
this.currentCard = cardDetails;
}
/**
* Handle PWA ready state changes
*/
pwaReadyValueChanged() {
if (this.pwaReadyValue) {
this.loadCard();
}
}
/**
* Load member profile data with retry logic
*/
async loadCard() {
if (!this.pwaReadyValue) return;
this.cardSetTarget.innerHTML = "";
this.loadingTarget.hidden = false;
this.memberDetailsTarget.hidden = true;
if (this.hasProfilePhotoContainerTarget) {
this.profilePhotoContainerTarget.hidden = true;
}
try {
// Use base class fetchWithRetry for reliability
const response = await this.fetchWithRetry(this.urlValue);
const data = await response.json();
this.loadingTarget.hidden = true;
this.memberDetailsTarget.hidden = false;
this.renderMemberData(data);
} catch (error) {
console.error("Error loading card:", error);
this.loadingTarget.hidden = true;
this.memberDetailsTarget.hidden = false;
this.nameTarget.textContent = "Error loading card data";
// Show retry button if offline
if (!this.online) {
this.showOfflineMessage();
}
}
}
/**
* Render member data to the card
*/
renderMemberData(data) {
this.nameTarget.textContent = data.member.first_name + ' ' + data.member.last_name;
this.scaNameTarget.textContent = data.member.sca_name;
this.branchNameTarget.textContent = data.member.branch.name;
// Membership info
if (data.member.membership_number && data.member.membership_number.length > 0) {
const memberExpDate = new Date(data.member.membership_expires_on);
const expText = memberExpDate < new Date() ? "Expired" : " - " + memberExpDate.toLocaleDateString();
this.membershipInfoTarget.textContent = data.member.membership_number + ' ' + expText;
} else {
this.membershipInfoTarget.textContent = "No Membership Info";
}
// Background check
if (data.member.background_check_expires_on) {
const bgCheckDate = new Date(data.member.background_check_expires_on);
const bgText = bgCheckDate < new Date() ? "Expired" : 'Current ' + bgCheckDate.toLocaleDateString();
this.backgroundCheckTarget.textContent = bgText;
} else {
this.backgroundCheckTarget.textContent = "Not on file";
}
this.lastUpdateTarget.textContent = new Date().toLocaleString();
this.renderProfilePhoto(data.member.profile_photo_url || null);
// Render plugin sections
this.renderPluginSections(data);
}
renderProfilePhoto(photoUrl) {
const hasPhoto = typeof photoUrl === "string" && photoUrl.length > 0;
if (this.hasProfilePhotoContainerTarget) {
this.profilePhotoContainerTarget.hidden = !hasPhoto;
}
if (hasPhoto) {
if (this.hasProfilePhotoTarget) {
this.profilePhotoTarget.src = photoUrl;
}
if (this.hasZoomPhotoTarget) {
this.zoomPhotoTarget.src = photoUrl;
}
this.cacheProfilePhotoForOffline(photoUrl);
}
}
async cacheProfilePhotoForOffline(photoUrl) {
if (!('serviceWorker' in navigator) || typeof photoUrl !== "string" || photoUrl.length === 0) {
return;
}
try {
const registration = await navigator.serviceWorker.ready;
registration?.active?.postMessage({
type: 'CACHE_URLS',
payload: [photoUrl]
});
} catch (error) {
console.warn('[member-mobile-card-profile] Could not queue profile photo for offline cache', error);
}
}
/**
* Render plugin-provided sections
*/
renderPluginSections(data) {
for (let key in data) {
if (key === 'member') continue;
const pluginData = data[key];
for (let sectionKey in pluginData) {
const sectionData = pluginData[sectionKey];
if (Object.keys(sectionData).length === 0) continue;
this.startCard(sectionKey);
const groupTable = document.createElement("table");
groupTable.classList.add("table", "card-body-table");
const groupTableBody = document.createElement("tbody");
groupTable.appendChild(groupTableBody);
for (let groupKey in sectionData) {
const groupData = sectionData[groupKey];
if (groupData.length === 0) continue;
// Group header
const headerRow = document.createElement("tr");
const groupHeader = document.createElement("th");
groupHeader.classList.add("col-12", "text-center");
groupHeader.colSpan = "2";
groupHeader.textContent = groupKey;
headerRow.appendChild(groupHeader);
groupTableBody.appendChild(headerRow);
// Group items
let colCount = 0;
let groupRow = document.createElement("tr");
for (let i = 0; i < groupData.length; i++) {
const itemData = groupData[i];
if (colCount === 2) {
groupTableBody.appendChild(groupRow);
groupRow = document.createElement("tr");
colCount = 0;
}
// Handle key:value format
if (itemData.indexOf(":") > 2) {
const itemValue = itemData.split(":");
const itemValueRow = document.createElement("tr");
const itemValueCol1 = document.createElement("td");
itemValueCol1.classList.add("col-6", "text-end");
itemValueCol1.textContent = itemValue[0];
const itemValueCol2 = document.createElement("td");
itemValueCol2.classList.add("col-6", "text-start");
itemValueCol2.textContent = itemValue[1];
itemValueRow.appendChild(itemValueCol1);
itemValueRow.appendChild(itemValueCol2);
groupTableBody.appendChild(itemValueRow);
} else {
const colspan = (i + 1 === groupData.length && colCount === 0) ? 2 : 1;
const itemValueCol = document.createElement("td");
itemValueCol.classList.add("col-6", "text-center");
itemValueCol.colSpan = colspan;
itemValueCol.textContent = itemData;
groupRow.appendChild(itemValueCol);
colCount++;
}
}
groupTableBody.appendChild(groupRow);
}
this.currentCard.appendChild(groupTable);
}
}
}
/**
* Show offline message with retry option
*/
showOfflineMessage() {
const offlineMsg = document.createElement("div");
offlineMsg.className = "alert alert-warning text-center m-3";
offlineMsg.innerHTML = `
<i class="bi bi-wifi-off me-2"></i>
Unable to load - you're offline
<button class="btn btn-sm btn-outline-warning ms-2" data-action="click->member-mobile-card-profile#retryLoad">
Retry
</button>
`;
this.cardSetTarget.appendChild(offlineMsg);
}
/**
* Retry loading card data
*/
retryLoad() {
this.loadCard();
}
/**
* Hide photo capture/upload affordances while offline.
*
* @param {boolean} isOnline Current connectivity state.
*/
updatePhotoActionsForConnection(isOnline) {
if (this.hasPhotoManageButtonTarget) {
this.photoManageButtonTarget.hidden = !isOnline;
}
if (!isOnline) {
this.hidePhotoUploadModal();
}
}
hidePhotoUploadModal() {
if (!this.hasPhotoUploadModalTarget || !window.bootstrap?.Modal) {
return;
}
const modalElement = this.photoUploadModalTarget;
const existingModal = window.bootstrap.Modal.getInstance?.(modalElement);
if (existingModal) {
existingModal.hide();
return;
}
const modal = window.bootstrap.Modal.getOrCreateInstance?.(modalElement);
modal?.hide();
}
}
if (!window.Controllers) {
window.Controllers = {};
}
window.Controllers["member-mobile-card-profile"] = MemberMobileCardProfile;