abmp-npm 1.8.35 → 1.8.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/backend/index.js CHANGED
@@ -8,4 +8,5 @@ module.exports = {
8
8
  ...require('./members-area-methods'), //TODO: remove it once we finish NPM movement
9
9
  ...require('./members-data-methods'), //TODO: remove it once we finish NPM movement
10
10
  ...require('./cms-data-methods'), //TODO: remove it once we finish NPM movement
11
+ ...require('./routers-methods'), //TODO: remove it once we finish NPM movement
11
12
  };
@@ -0,0 +1,351 @@
1
+ const { DEFAULT_SEO_DESCRIPTION, ADDRESS_STATUS_TYPES } = require('../public/consts');
2
+
3
+ const { getMemberBySlug } = require('./members-data-methods');
4
+ const { formatDateToMonthYear } = require('./utils');
5
+
6
+ /**
7
+ * Generates SEO title for member profile
8
+ * @param {string} fullName - Member's full name
9
+ * @param {Array<string>} areasOfPractices - Member's areas of practice
10
+ * @returns {string} SEO title
11
+ */
12
+ function generateSEOTitle(fullName, areasOfPractices) {
13
+ return `${fullName}${
14
+ areasOfPractices && areasOfPractices.length > 0
15
+ ? ` | ${areasOfPractices.slice(0, 3).join(', ')}`
16
+ : ''
17
+ } | ABMP Member`;
18
+ }
19
+
20
+ /**
21
+ * Strips HTML tags and decodes HTML entities from a string
22
+ * @param {string} html - HTML string to clean
23
+ * @returns {string} Cleaned text
24
+ */
25
+ function stripHtmlTags(html) {
26
+ if (!html) return '';
27
+ // Remove HTML tags and decode HTML entities
28
+ return html
29
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
30
+ .replace(/&nbsp;/g, ' ') // Replace non-breaking spaces
31
+ .replace(/&amp;/g, '&') // Replace encoded ampersands
32
+ .replace(/&lt;/g, '<') // Replace encoded less than
33
+ .replace(/&gt;/g, '>') // Replace encoded greater than
34
+ .replace(/&quot;/g, '"') // Replace encoded quotes
35
+ .replace(/&#39;/g, "'") // Replace encoded apostrophes
36
+ .replace(/\s+/g, ' ') // Replace multiple whitespace with single space
37
+ .trim(); // Remove leading/trailing whitespace
38
+ }
39
+
40
+ /**
41
+ * Check if member has student membership
42
+ * @param {Object} member - Member object
43
+ * @param {boolean} checkAssociation - Whether to check for specific association
44
+ * @param {string} siteAssociation - Site association to check for
45
+ * @param {string} studentType - Student membership type
46
+ * @returns {boolean} True if member has student membership
47
+ */
48
+ function hasStudentMembership(member, checkAssociation, siteAssociation, studentType) {
49
+ const memberships = member?.memberships;
50
+ if (!Array.isArray(memberships)) return false;
51
+
52
+ return memberships.some(membership => {
53
+ const isStudent = membership.membertype === studentType;
54
+ const hasCorrectAssociation = !checkAssociation || membership.association === siteAssociation;
55
+ return isStudent && hasCorrectAssociation;
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Check if member should have student badge
61
+ * @param {Object} member - Member object
62
+ * @param {string} siteAssociation - Site association
63
+ * @param {string} studentType - Student membership type
64
+ * @returns {boolean} True if should have badge
65
+ */
66
+ function shouldHaveStudentBadge(member, siteAssociation, studentType) {
67
+ return hasStudentMembership(member, true, siteAssociation, studentType);
68
+ }
69
+
70
+ /**
71
+ * Get addresses by status, excluding main address
72
+ * @param {Array} addresses - All addresses
73
+ * @param {Array} addressDisplayOption - Display options
74
+ * @returns {Array} Processed addresses
75
+ */
76
+ function getAddressesByStatus(
77
+ addresses = [],
78
+ addressDisplayOption = [],
79
+ formatAddress,
80
+ getMainAddress,
81
+ generateId
82
+ ) {
83
+ const visible = addresses.filter(addr => addr.addressStatus !== ADDRESS_STATUS_TYPES.DONT_SHOW);
84
+ if (visible.length < 2) {
85
+ return [];
86
+ }
87
+ const opts = Array.isArray(addressDisplayOption) ? addressDisplayOption : [];
88
+ const mainOpt = opts.find(o => o.isMain);
89
+ const mainKey = mainOpt ? mainOpt.key : visible[0].key;
90
+ return visible
91
+ .filter(addr => addr?.key !== mainKey)
92
+ .map(addr => {
93
+ const addressString = formatAddress(addr);
94
+ return addressString ? { _id: generateId(), address: addressString } : null;
95
+ })
96
+ .filter(Boolean);
97
+ }
98
+
99
+ /**
100
+ * Get member profile data formatted for display
101
+ * @param {Object} member - Member object
102
+ * @param {Object} utils - Utility functions (formatAddress, getMainAddress, generateId)
103
+ * @param {Object} constants - Constants (siteAssociation, studentType, pacStaffType)
104
+ * @returns {Object} Formatted profile data
105
+ */
106
+ function getMemberProfileData(member, utils, constants) {
107
+ if (!member) {
108
+ throw new Error('member is required');
109
+ }
110
+ const { formatAddress, getMainAddress, generateId } = utils;
111
+ const { siteAssociation, studentType, pacStaffType } = constants;
112
+
113
+ const addresses = member.addresses || [];
114
+ const licenceNo = member.licenses
115
+ ?.map(val => val.license)
116
+ .filter(Boolean)
117
+ .join(', ');
118
+ const processedAddresses = getAddressesByStatus(
119
+ member.addresses,
120
+ member.addressDisplayOption,
121
+ formatAddress,
122
+ getMainAddress,
123
+ generateId
124
+ );
125
+
126
+ const memberships = member.memberships || [];
127
+ const abmp = memberships.find(m => m.association === siteAssociation);
128
+
129
+ const areasOfPractices =
130
+ member.areasOfPractices
131
+ ?.filter(item => typeof item === 'string' && item.trim().length > 0)
132
+ .map(item => item.trim())
133
+ .sort((a, b) =>
134
+ a.localeCompare(b, undefined, {
135
+ sensitivity: 'base',
136
+ numeric: true,
137
+ })
138
+ ) || [];
139
+
140
+ const mainAddress = getMainAddress(member.addressDisplayOption, addresses);
141
+
142
+ return {
143
+ mainAddress: mainAddress,
144
+ testimonials: member.testimonial || [],
145
+ licenceNo,
146
+ processedAddresses,
147
+ memberSince: (member.showABMP && abmp && formatDateToMonthYear(abmp?.membersince)) || '',
148
+ shouldHaveStudentBadge: shouldHaveStudentBadge(member, siteAssociation, studentType),
149
+ logoImage: member.logoImage,
150
+ fullName: member.fullName,
151
+ profileImage: member.profileImage,
152
+ showContactForm: member.showContactForm,
153
+ bookingUrl: member.bookingUrl,
154
+ aboutService: member.aboutService,
155
+ businessName: (member.showBusinessName && member.businessName) || '',
156
+ phone: member.toShowPhone || '',
157
+ areasOfPractices,
158
+ gallery: member.gallery,
159
+ bannerImages: member.bannerImages,
160
+ showWixUrl: member.showWixUrl,
161
+ _id: member._id,
162
+ url: member.url,
163
+ city: mainAddress?.city || '',
164
+ state: mainAddress?.state || '',
165
+ isPrivateMember: member.memberships.some(membership => membership.membertype === pacStaffType),
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Profile router handler
171
+ * @param {Object} request - Router request object
172
+ * @param {Object} dependencies - Dependencies (ok, notFound, redirect, sendStatus)
173
+ * @param {Object} utils - Utility functions
174
+ * @param {Object} constants - Constants
175
+ * @returns {Promise} Router response
176
+ */
177
+ async function profileRouter(request, dependencies, utils, constants) {
178
+ const { ok, notFound, redirect, sendStatus } = dependencies;
179
+ const { abmpLogoUrl } = constants;
180
+
181
+ const slug = request.path[0];
182
+ if (!slug) {
183
+ return redirect(request.baseUrl);
184
+ }
185
+ try {
186
+ const member = await getMemberBySlug({
187
+ slug,
188
+ excludeDropped: true,
189
+ excludeSearchedMember: false,
190
+ });
191
+
192
+ if (!member) {
193
+ return notFound();
194
+ }
195
+
196
+ const profileData = getMemberProfileData(member, utils, constants);
197
+
198
+ if (profileData && profileData.showWixUrl) {
199
+ const ogImage = profileData.profileImage || profileData.logoImage || abmpLogoUrl;
200
+ const seoTitle = generateSEOTitle(profileData.fullName, profileData.areasOfPractices);
201
+ // Use stripped HTML from aboutService rich text content
202
+ let description = stripHtmlTags(profileData.aboutService) || DEFAULT_SEO_DESCRIPTION;
203
+
204
+ // Limit description to 160 characters for optimal SEO
205
+ if (description.length > 160) {
206
+ description = description.substring(0, 157) + '...';
207
+ }
208
+ const profileUrl = `https://www.abmpmembers.com/profile/${profileData.url}`;
209
+ const isPrivateMember = profileData.isPrivateMember;
210
+ const seoData = {
211
+ title: seoTitle,
212
+ description: description,
213
+ noIndex: isPrivateMember,
214
+ metaTags: [
215
+ {
216
+ name: 'description',
217
+ content: description,
218
+ },
219
+ {
220
+ name: 'keywords',
221
+ content:
222
+ `${profileData.fullName}, ${profileData.areasOfPractices ? profileData.areasOfPractices.slice(0, 3).join(', ') : ''}, ABMP, ${profileData.city || ''}, ${profileData.state || ''}`
223
+ .replace(/,\s*,/g, ',')
224
+ .replace(/^,|,$/g, ''),
225
+ },
226
+ {
227
+ name: 'author',
228
+ content: profileData.fullName,
229
+ },
230
+ {
231
+ name: 'robots',
232
+ content: isPrivateMember ? 'noindex, nofollow' : 'index, follow',
233
+ },
234
+ // Open Graph tags
235
+ {
236
+ property: 'og:type',
237
+ content: 'profile',
238
+ },
239
+ {
240
+ property: 'og:title',
241
+ content: seoTitle,
242
+ },
243
+ {
244
+ property: 'og:description',
245
+ content: description,
246
+ },
247
+ {
248
+ property: 'og:image',
249
+ content: ogImage,
250
+ },
251
+ {
252
+ property: 'og:url',
253
+ content: profileUrl,
254
+ },
255
+ {
256
+ property: 'og:site_name',
257
+ content: 'ABMP Members',
258
+ },
259
+ // Twitter Card tags
260
+ {
261
+ name: 'twitter:card',
262
+ content: 'summary_large_image',
263
+ },
264
+ {
265
+ name: 'twitter:title',
266
+ content: seoTitle,
267
+ },
268
+ {
269
+ name: 'twitter:description',
270
+ content: description,
271
+ },
272
+ {
273
+ name: 'twitter:image',
274
+ content: ogImage,
275
+ },
276
+ // Additional SEO tags
277
+ {
278
+ name: 'geo.region',
279
+ content: profileData.state || '',
280
+ },
281
+ {
282
+ name: 'geo.placename',
283
+ content: profileData.city || '',
284
+ },
285
+ ].filter(tag => tag.content && tag.content.trim() !== ''), // Remove empty tags
286
+ };
287
+ return ok('profile', profileData, seoData);
288
+ }
289
+ return notFound();
290
+ } catch (error) {
291
+ console.error(error);
292
+ return sendStatus('500', 'Internal Server Error');
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Profile sitemap generator
298
+ * @param {Object} _sitemapRequest - Sitemap request object
299
+ * @param {Object} _dependencies - Dependencies (WixRouterSitemapEntry)
300
+ * @param {Function} _fetchAllItemsInParallel - Function to fetch all items in parallel
301
+ * @returns {Array} Sitemap entries
302
+ */
303
+ function profileSiteMap(_sitemapRequest, _dependencies, _fetchAllItemsInParallel) {
304
+ return [];
305
+ // Commented out - currently disabled in host site
306
+ /*
307
+ const { WixRouterSitemapEntry, wixData } = dependencies;
308
+
309
+ try {
310
+ const membersQuery = wixData
311
+ .query(COLLECTIONS.MEMBERS_DATA)
312
+ .eq('showWixUrl', true)
313
+ .isNotEmpty('url')
314
+ .ne('action', 'drop')
315
+ .fields('url', 'fullName');
316
+
317
+ const allMembers = await fetchAllItemsInParallel(membersQuery);
318
+
319
+ const batchSize = 1000;
320
+ const sitemapEntries = [];
321
+ const totalItems = allMembers.items.length;
322
+
323
+ for (let i = 0; i < totalItems; i += batchSize) {
324
+ const batch = allMembers.items.slice(i, i + batchSize);
325
+ const batchEntries = batch.map(member => {
326
+ const entry = new WixRouterSitemapEntry(member.fullName);
327
+ entry.pageName = 'profile';
328
+ entry.url = `profile/${member.url}`;
329
+ entry.title = member.fullName;
330
+ entry.changeFrequency = 'monthly';
331
+ entry.priority = 1.0;
332
+ return entry;
333
+ });
334
+ sitemapEntries.push(...batchEntries);
335
+ }
336
+
337
+ return sitemapEntries;
338
+ } catch (error) {
339
+ console.error('Error generating profile sitemap:', error);
340
+ return [];
341
+ }
342
+ */
343
+ }
344
+
345
+ module.exports = {
346
+ profileRouter,
347
+ profileSiteMap,
348
+ getMemberProfileData,
349
+ generateSEOTitle,
350
+ stripHtmlTags,
351
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "1.8.35",
3
+ "version": "1.8.37",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
package/pages/Profile.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { location: wixLocation } = require('@wix/site-location');
2
2
  const { window: wixWindow } = require('@wix/site-window');
3
3
 
4
+ const { DEFAULT_PROFILE_IMAGE, LIGHTBOX_NAMES } = require('../public/consts');
4
5
  const { generateId, formatPracticeAreasForDisplay } = require('../public/Utils/sharedUtils');
5
6
 
6
7
  const TESTIMONIALS_PER_PAGE_CONFIG = {
@@ -119,8 +120,7 @@ async function profileOnReady({ $w: _$w }) {
119
120
  if (profileData.profileImage) {
120
121
  _$w('#profileImage').src = profileData.profileImage;
121
122
  } else {
122
- _$w('#profileImage').src =
123
- 'https://static.wixstatic.com/media/1d7134_e052e9b1d0a543d0980650e16dd6d374~mv2.jpg';
123
+ _$w('#profileImage').src = DEFAULT_PROFILE_IMAGE;
124
124
  }
125
125
  }
126
126
 
@@ -145,7 +145,9 @@ async function profileOnReady({ $w: _$w }) {
145
145
 
146
146
  function bindContactForm() {
147
147
  if (profileData.showContactForm) {
148
- _$w('#contactButton').onClick(() => wixWindow.openLightbox('Contact Us', profileData));
148
+ _$w('#contactButton').onClick(() =>
149
+ wixWindow.openLightbox(LIGHTBOX_NAMES.CONTACT_US, profileData)
150
+ );
149
151
  } else {
150
152
  _$w('#contactButton').collapse();
151
153
  }
@@ -9,7 +9,7 @@ const {
9
9
  FREE_WEBSITE_TEXT_STATES,
10
10
  LIGHTBOX_NAMES,
11
11
  } = require('../public/consts');
12
- const { handleOnCustomValidation, isNotValidUrl } = require('../public/Utils/profilePageUtils');
12
+ const { handleOnCustomValidation, isNotValidUrl } = require('../public/Utils/personalDetailsUtils');
13
13
  const { generateId } = require('../public/Utils/sharedUtils');
14
14
 
15
15
  const MAX_PHONES_COUNT = 10;
package/public/consts.js CHANGED
@@ -80,6 +80,7 @@ const LIGHTBOX_NAMES = {
80
80
  LOGIN_ERROR_ALERT: 'loginError',
81
81
  DELETE_CONFIRM: 'deleteConfirm',
82
82
  SELECT_BANNER_IMAGES: 'Select Banner Images',
83
+ CONTACT_US: 'Contact Us',
83
84
  };
84
85
 
85
86
  const ABMP_MEMBERS_HOME_URL = 'https://www.abmp.com/members';
@@ -91,6 +92,8 @@ const FREE_WEBSITE_TEXT_STATES = {
91
92
 
92
93
  const DEFAULT_BUSINESS_NAME_TEXT = 'Business name not provided';
93
94
 
95
+ const DEFAULT_PROFILE_IMAGE =
96
+ 'https://static.wixstatic.com/media/1d7134_e052e9b1d0a543d0980650e16dd6d374~mv2.jpg';
94
97
  module.exports = {
95
98
  REGEX,
96
99
  COLLECTIONS,
@@ -103,4 +106,5 @@ module.exports = {
103
106
  ABMP_MEMBERS_HOME_URL,
104
107
  FREE_WEBSITE_TEXT_STATES,
105
108
  DEFAULT_BUSINESS_NAME_TEXT,
109
+ DEFAULT_PROFILE_IMAGE,
106
110
  };
package/public/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  module.exports = {
2
2
  ...require('./consts'),
3
3
  ...require('./messages'),
4
- ...require('./Utils/sharedUtils'),
5
4
  };