abmp-npm 1.8.36 → 1.8.38

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