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 +1 -0
- package/backend/routers-methods.js +351 -0
- package/package.json +1 -1
- package/pages/Profile.js +5 -3
- package/pages/personalDetails.js +1 -1
- package/public/consts.js +4 -0
- package/public/index.js +0 -1
- /package/public/Utils/{profilePageUtils.js → personalDetailsUtils.js} +0 -0
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(/ /g, ' ') // Replace non-breaking spaces
|
|
31
|
+
.replace(/&/g, '&') // Replace encoded ampersands
|
|
32
|
+
.replace(/</g, '<') // Replace encoded less than
|
|
33
|
+
.replace(/>/g, '>') // Replace encoded greater than
|
|
34
|
+
.replace(/"/g, '"') // Replace encoded quotes
|
|
35
|
+
.replace(/'/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
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(() =>
|
|
148
|
+
_$w('#contactButton').onClick(() =>
|
|
149
|
+
wixWindow.openLightbox(LIGHTBOX_NAMES.CONTACT_US, profileData)
|
|
150
|
+
);
|
|
149
151
|
} else {
|
|
150
152
|
_$w('#contactButton').collapse();
|
|
151
153
|
}
|
package/pages/personalDetails.js
CHANGED
|
@@ -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/
|
|
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
|
File without changes
|