abmp-npm 1.8.37 → 1.8.39
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/routers-methods.js +6 -171
- package/backend/routers-utils.js +164 -0
- package/package.json +1 -1
- package/public/consts.js +14 -0
- package/public/index.js +1 -0
|
@@ -1,182 +1,16 @@
|
|
|
1
|
-
const { DEFAULT_SEO_DESCRIPTION,
|
|
1
|
+
const { DEFAULT_SEO_DESCRIPTION, ABMP_LOGO_URL } = require('../public');
|
|
2
2
|
|
|
3
3
|
const { getMemberBySlug } = require('./members-data-methods');
|
|
4
|
-
const {
|
|
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
|
-
}
|
|
4
|
+
const { generateSEOTitle, stripHtmlTags, getMemberProfileData } = require('./routers-utils');
|
|
168
5
|
|
|
169
6
|
/**
|
|
170
7
|
* Profile router handler
|
|
171
8
|
* @param {Object} request - Router request object
|
|
172
9
|
* @param {Object} dependencies - Dependencies (ok, notFound, redirect, sendStatus)
|
|
173
|
-
* @param {Object} utils - Utility functions
|
|
174
|
-
* @param {Object} constants - Constants
|
|
175
10
|
* @returns {Promise} Router response
|
|
176
11
|
*/
|
|
177
|
-
async function profileRouter(request, dependencies
|
|
12
|
+
async function profileRouter(request, dependencies) {
|
|
178
13
|
const { ok, notFound, redirect, sendStatus } = dependencies;
|
|
179
|
-
const { abmpLogoUrl } = constants;
|
|
180
14
|
|
|
181
15
|
const slug = request.path[0];
|
|
182
16
|
if (!slug) {
|
|
@@ -193,10 +27,10 @@ async function profileRouter(request, dependencies, utils, constants) {
|
|
|
193
27
|
return notFound();
|
|
194
28
|
}
|
|
195
29
|
|
|
196
|
-
const profileData = getMemberProfileData(member
|
|
30
|
+
const profileData = getMemberProfileData(member);
|
|
197
31
|
|
|
198
32
|
if (profileData && profileData.showWixUrl) {
|
|
199
|
-
const ogImage = profileData.profileImage || profileData.logoImage ||
|
|
33
|
+
const ogImage = profileData.profileImage || profileData.logoImage || ABMP_LOGO_URL;
|
|
200
34
|
const seoTitle = generateSEOTitle(profileData.fullName, profileData.areasOfPractices);
|
|
201
35
|
// Use stripped HTML from aboutService rich text content
|
|
202
36
|
let description = stripHtmlTags(profileData.aboutService) || DEFAULT_SEO_DESCRIPTION;
|
|
@@ -345,6 +179,7 @@ function profileSiteMap(_sitemapRequest, _dependencies, _fetchAllItemsInParallel
|
|
|
345
179
|
module.exports = {
|
|
346
180
|
profileRouter,
|
|
347
181
|
profileSiteMap,
|
|
182
|
+
// Re-export utilities for backward compatibility
|
|
348
183
|
getMemberProfileData,
|
|
349
184
|
generateSEOTitle,
|
|
350
185
|
stripHtmlTags,
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const {
|
|
2
|
+
ADDRESS_STATUS_TYPES,
|
|
3
|
+
SITE_ASSOCIATION,
|
|
4
|
+
MEMBERSHIPS_TYPES,
|
|
5
|
+
formatAddress,
|
|
6
|
+
getMainAddress,
|
|
7
|
+
generateId,
|
|
8
|
+
} = require('../public');
|
|
9
|
+
|
|
10
|
+
const { formatDateToMonthYear } = require('./utils');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates SEO title for member profile
|
|
14
|
+
* @param {string} fullName - Member's full name
|
|
15
|
+
* @param {Array<string>} areasOfPractices - Member's areas of practice
|
|
16
|
+
* @returns {string} SEO title
|
|
17
|
+
*/
|
|
18
|
+
function generateSEOTitle(fullName, areasOfPractices) {
|
|
19
|
+
return `${fullName}${
|
|
20
|
+
areasOfPractices && areasOfPractices.length > 0
|
|
21
|
+
? ` | ${areasOfPractices.slice(0, 3).join(', ')}`
|
|
22
|
+
: ''
|
|
23
|
+
} | ABMP Member`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Strips HTML tags and decodes HTML entities from a string
|
|
28
|
+
* @param {string} html - HTML string to clean
|
|
29
|
+
* @returns {string} Cleaned text
|
|
30
|
+
*/
|
|
31
|
+
function stripHtmlTags(html) {
|
|
32
|
+
if (!html) return '';
|
|
33
|
+
// Remove HTML tags and decode HTML entities
|
|
34
|
+
return html
|
|
35
|
+
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
36
|
+
.replace(/ /g, ' ') // Replace non-breaking spaces
|
|
37
|
+
.replace(/&/g, '&') // Replace encoded ampersands
|
|
38
|
+
.replace(/</g, '<') // Replace encoded less than
|
|
39
|
+
.replace(/>/g, '>') // Replace encoded greater than
|
|
40
|
+
.replace(/"/g, '"') // Replace encoded quotes
|
|
41
|
+
.replace(/'/g, "'") // Replace encoded apostrophes
|
|
42
|
+
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
|
|
43
|
+
.trim(); // Remove leading/trailing whitespace
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if member has student membership
|
|
48
|
+
* @param {Object} member - Member object
|
|
49
|
+
* @param {boolean} checkAssociation - Whether to check for specific association
|
|
50
|
+
* @returns {boolean} True if member has student membership
|
|
51
|
+
*/
|
|
52
|
+
function hasStudentMembership(member, checkAssociation) {
|
|
53
|
+
const memberships = member?.memberships;
|
|
54
|
+
if (!Array.isArray(memberships)) return false;
|
|
55
|
+
|
|
56
|
+
return memberships.some(membership => {
|
|
57
|
+
const isStudent = membership.membertype === MEMBERSHIPS_TYPES.STUDENT;
|
|
58
|
+
const hasCorrectAssociation = !checkAssociation || membership.association === SITE_ASSOCIATION;
|
|
59
|
+
return isStudent && hasCorrectAssociation;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if member should have student badge
|
|
65
|
+
* @param {Object} member - Member object
|
|
66
|
+
* @returns {boolean} True if should have badge
|
|
67
|
+
*/
|
|
68
|
+
function shouldHaveStudentBadge(member) {
|
|
69
|
+
return hasStudentMembership(member, true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get addresses by status, excluding main address
|
|
74
|
+
* @param {Array} addresses - All addresses
|
|
75
|
+
* @param {Array} addressDisplayOption - Display options
|
|
76
|
+
* @returns {Array} Processed addresses
|
|
77
|
+
*/
|
|
78
|
+
function getAddressesByStatus(addresses = [], addressDisplayOption = []) {
|
|
79
|
+
const visible = addresses.filter(addr => addr.addressStatus !== ADDRESS_STATUS_TYPES.DONT_SHOW);
|
|
80
|
+
if (visible.length < 2) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const opts = Array.isArray(addressDisplayOption) ? addressDisplayOption : [];
|
|
84
|
+
const mainOpt = opts.find(o => o.isMain);
|
|
85
|
+
const mainKey = mainOpt ? mainOpt.key : visible[0].key;
|
|
86
|
+
return visible
|
|
87
|
+
.filter(addr => addr?.key !== mainKey)
|
|
88
|
+
.map(addr => {
|
|
89
|
+
const addressString = formatAddress(addr);
|
|
90
|
+
return addressString ? { _id: generateId(), address: addressString } : null;
|
|
91
|
+
})
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get member profile data formatted for display
|
|
97
|
+
* @param {Object} member - Member object
|
|
98
|
+
* @returns {Object} Formatted profile data
|
|
99
|
+
*/
|
|
100
|
+
function getMemberProfileData(member) {
|
|
101
|
+
if (!member) {
|
|
102
|
+
throw new Error('member is required');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const addresses = member.addresses || [];
|
|
106
|
+
const licenceNo = member.licenses
|
|
107
|
+
?.map(val => val.license)
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(', ');
|
|
110
|
+
const processedAddresses = getAddressesByStatus(member.addresses, member.addressDisplayOption);
|
|
111
|
+
|
|
112
|
+
const memberships = member.memberships || [];
|
|
113
|
+
const abmp = memberships.find(m => m.association === SITE_ASSOCIATION);
|
|
114
|
+
|
|
115
|
+
const areasOfPractices =
|
|
116
|
+
member.areasOfPractices
|
|
117
|
+
?.filter(item => typeof item === 'string' && item.trim().length > 0)
|
|
118
|
+
.map(item => item.trim())
|
|
119
|
+
.sort((a, b) =>
|
|
120
|
+
a.localeCompare(b, undefined, {
|
|
121
|
+
sensitivity: 'base',
|
|
122
|
+
numeric: true,
|
|
123
|
+
})
|
|
124
|
+
) || [];
|
|
125
|
+
|
|
126
|
+
const mainAddress = getMainAddress(member.addressDisplayOption, addresses);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
mainAddress: mainAddress,
|
|
130
|
+
testimonials: member.testimonial || [],
|
|
131
|
+
licenceNo,
|
|
132
|
+
processedAddresses,
|
|
133
|
+
memberSince: (member.showABMP && abmp && formatDateToMonthYear(abmp?.membersince)) || '',
|
|
134
|
+
shouldHaveStudentBadge: shouldHaveStudentBadge(member),
|
|
135
|
+
logoImage: member.logoImage,
|
|
136
|
+
fullName: member.fullName,
|
|
137
|
+
profileImage: member.profileImage,
|
|
138
|
+
showContactForm: member.showContactForm,
|
|
139
|
+
bookingUrl: member.bookingUrl,
|
|
140
|
+
aboutService: member.aboutService,
|
|
141
|
+
businessName: (member.showBusinessName && member.businessName) || '',
|
|
142
|
+
phone: member.toShowPhone || '',
|
|
143
|
+
areasOfPractices,
|
|
144
|
+
gallery: member.gallery,
|
|
145
|
+
bannerImages: member.bannerImages,
|
|
146
|
+
showWixUrl: member.showWixUrl,
|
|
147
|
+
_id: member._id,
|
|
148
|
+
url: member.url,
|
|
149
|
+
city: mainAddress?.city || '',
|
|
150
|
+
state: mainAddress?.state || '',
|
|
151
|
+
isPrivateMember: member.memberships.some(
|
|
152
|
+
membership => membership.membertype === MEMBERSHIPS_TYPES.PAC_STAFF
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
generateSEOTitle,
|
|
159
|
+
stripHtmlTags,
|
|
160
|
+
hasStudentMembership,
|
|
161
|
+
shouldHaveStudentBadge,
|
|
162
|
+
getAddressesByStatus,
|
|
163
|
+
getMemberProfileData,
|
|
164
|
+
};
|
package/package.json
CHANGED
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
|
};
|