abmp-npm 1.9.2 → 1.9.5
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/cms-data-methods.js +8 -0
- package/backend/elevated-modules.js +1 -0
- package/backend/http-functions/httpFunctions.js +72 -0
- package/backend/http-functions/index.js +3 -0
- package/backend/http-functions/interests.js +37 -0
- package/backend/index.js +2 -0
- package/backend/members-data-methods.js +34 -0
- package/backend/qa-login-methods.js +67 -0
- package/backend/routers/index.js +3 -0
- package/backend/routers/methods.js +177 -0
- package/backend/routers/utils.js +118 -0
- package/backend/utils.js +38 -13
- package/package.json +1 -1
- package/pages/QAPage.js +39 -0
- package/pages/deleteConfirm.js +19 -0
- package/pages/index.js +2 -1
- package/public/Utils/sharedUtils.js +1 -0
- package/public/consts.js +1 -3
- package/public/index.js +1 -0
- package/pages/SaveAlerts.js +0 -13
|
@@ -231,9 +231,17 @@ async function getInterestAll() {
|
|
|
231
231
|
throw e;
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
async function clearCollection(collectionName) {
|
|
235
|
+
try {
|
|
236
|
+
await wixData.truncate(collectionName);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
throw new Error(`Failed to clearCollection ${collectionName} with error: ${err.message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
234
241
|
|
|
235
242
|
module.exports = {
|
|
236
243
|
buildMembersSearchQuery,
|
|
237
244
|
fetchAllItemsInParallel,
|
|
238
245
|
getInterestAll,
|
|
246
|
+
clearCollection,
|
|
239
247
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { COLLECTIONS } = require('../../public/consts');
|
|
2
|
+
const { clearCollection } = require('../cms-data-methods');
|
|
3
|
+
const { getSecret } = require('../utils');
|
|
4
|
+
|
|
5
|
+
const { migrateInterests } = require('./interests');
|
|
6
|
+
|
|
7
|
+
const createHTTPFunctionsHelpers = wixHTTPFunctionsMethods => {
|
|
8
|
+
const { created, serverError, forbidden, ok, badRequest } = wixHTTPFunctionsMethods;
|
|
9
|
+
const responseOptions = {
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const isRequestAuthenticated = async request => {
|
|
16
|
+
const AUTH_TOKEN = await getSecret('migrate-api-key');
|
|
17
|
+
return request.headers.authorization === 'Bearer ' + AUTH_TOKEN;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const withAuth = handler => async request => {
|
|
21
|
+
if (!(await isRequestAuthenticated(request))) {
|
|
22
|
+
return forbidden({
|
|
23
|
+
...responseOptions,
|
|
24
|
+
body: { error: 'Unauthorized' },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return handler(request);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const migrateInterestsHandler = async _request => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await migrateInterests();
|
|
33
|
+
return created({
|
|
34
|
+
...responseOptions,
|
|
35
|
+
body: {
|
|
36
|
+
result,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error migrating interests:', error);
|
|
41
|
+
return serverError(error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const clearCollectionHandler = async request => {
|
|
46
|
+
try {
|
|
47
|
+
const collectionName = request.query.collectionName;
|
|
48
|
+
if (!collectionName || !Object.values(COLLECTIONS).includes(collectionName)) {
|
|
49
|
+
return badRequest({
|
|
50
|
+
...responseOptions,
|
|
51
|
+
body: { error: 'Invalid collection name' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const result = await clearCollection(collectionName);
|
|
55
|
+
return ok({
|
|
56
|
+
...responseOptions,
|
|
57
|
+
body: {
|
|
58
|
+
result,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return serverError(error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
post_migrateInterests: withAuth(migrateInterestsHandler),
|
|
68
|
+
delete_clearCollection: withAuth(clearCollectionHandler),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
module.exports = { createHTTPFunctionsHelpers };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { COLLECTIONS } = require('../../public/consts');
|
|
2
|
+
const { clearCollection } = require('../cms-data-methods');
|
|
3
|
+
const { CONFIG_KEYS } = require('../consts');
|
|
4
|
+
const { wixData } = require('../elevated-modules');
|
|
5
|
+
const { getHeaders } = require('../pac-api-methods');
|
|
6
|
+
const { getSiteConfigs } = require('../utils');
|
|
7
|
+
|
|
8
|
+
const getInterests = async () => {
|
|
9
|
+
const [url, headers] = await Promise.all([
|
|
10
|
+
getSiteConfigs(CONFIG_KEYS.INTERESTS_API_URL),
|
|
11
|
+
getHeaders(),
|
|
12
|
+
]);
|
|
13
|
+
const fetchOptions = {
|
|
14
|
+
method: 'get',
|
|
15
|
+
headers: headers,
|
|
16
|
+
};
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url, fetchOptions);
|
|
19
|
+
return await response.json();
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error('Error getting interests:', e);
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function migrateInterests() {
|
|
27
|
+
const [interests, _] = await Promise.all([
|
|
28
|
+
getInterests(),
|
|
29
|
+
clearCollection(COLLECTIONS.INTERESTS),
|
|
30
|
+
]);
|
|
31
|
+
const interestData = interests.map(val => ({ title: val.interest }));
|
|
32
|
+
return await wixData.bulkInsert(COLLECTIONS.INTERESTS, interestData);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
migrateInterests,
|
|
37
|
+
};
|
package/backend/index.js
CHANGED
|
@@ -9,6 +9,8 @@ module.exports = {
|
|
|
9
9
|
...require('./members-area-methods'), //TODO: remove it once we finish NPM movement
|
|
10
10
|
...require('./members-data-methods'), //TODO: remove it once we finish NPM movement
|
|
11
11
|
...require('./cms-data-methods'), //TODO: remove it once we finish NPM movement
|
|
12
|
+
...require('./routers'),
|
|
12
13
|
...require('./sso-methods'),
|
|
13
14
|
...require('./data-hooks'),
|
|
15
|
+
...require('./http-functions'),
|
|
14
16
|
};
|
|
@@ -421,6 +421,38 @@ const getMembersByIds = async memberIds => {
|
|
|
421
421
|
}
|
|
422
422
|
};
|
|
423
423
|
|
|
424
|
+
const getMemberByEmail = async email => {
|
|
425
|
+
try {
|
|
426
|
+
const members = await wixData
|
|
427
|
+
.query(COLLECTIONS.MEMBERS_DATA)
|
|
428
|
+
.eq('email', email)
|
|
429
|
+
.limit(2)
|
|
430
|
+
.find()
|
|
431
|
+
.then(res => res.items);
|
|
432
|
+
if (members.length > 1) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`[getMemberByEmail] Multiple members found with email ${email} membersIds are : [${members.map(member => member.memberId).join(', ')}]`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return members[0] || null;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('Error getting member by email:', error);
|
|
440
|
+
throw new Error(`Failed to get member by email: ${error.message}`);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const getQAUsers = async () => {
|
|
445
|
+
try {
|
|
446
|
+
return await wixData
|
|
447
|
+
.query(COLLECTIONS.QA_USERS)
|
|
448
|
+
.include('member')
|
|
449
|
+
.find()
|
|
450
|
+
.then(res => res.items.map(item => item.member));
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error('Error getting QA users:', error);
|
|
453
|
+
throw new Error(`Failed to get QA users: ${error.message}`);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
424
456
|
async function getSiteMemberId(data) {
|
|
425
457
|
try {
|
|
426
458
|
console.log('data', data);
|
|
@@ -472,6 +504,8 @@ module.exports = {
|
|
|
472
504
|
getAllMembersWithoutContactFormEmail,
|
|
473
505
|
getAllUpdatedLoginEmails,
|
|
474
506
|
getMembersByIds,
|
|
507
|
+
getMemberByEmail,
|
|
508
|
+
getQAUsers,
|
|
475
509
|
getSiteMemberId,
|
|
476
510
|
checkUrlUniqueness,
|
|
477
511
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { authentication } = require('@wix/members');
|
|
2
|
+
|
|
3
|
+
const { getMemberByEmail, getQAUsers } = require('./members-data-methods');
|
|
4
|
+
const { getSecret } = require('./utils');
|
|
5
|
+
|
|
6
|
+
const validateQAUser = async userEmail => {
|
|
7
|
+
const qaUsers = await getQAUsers();
|
|
8
|
+
const matchingUser = qaUsers.find(user => user.email === userEmail);
|
|
9
|
+
if (!matchingUser) {
|
|
10
|
+
return { error: `Invalid user email: ${userEmail}` };
|
|
11
|
+
}
|
|
12
|
+
return { valid: true, user: matchingUser };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const loginQAMember = async (userEmail, secret) => {
|
|
16
|
+
try {
|
|
17
|
+
const userValidation = await validateQAUser(userEmail);
|
|
18
|
+
if (userValidation.error) {
|
|
19
|
+
return { success: false, error: userValidation.error };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const qaSecret = await getSecret('ABMP_QA_SECRET');
|
|
23
|
+
if (secret !== qaSecret) {
|
|
24
|
+
return { success: false, error: 'Invalid secret' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//TODO: this code still needs fixes, as there is no generateSessionToken method on
|
|
28
|
+
const token = await authentication.generateSessionToken(userValidation.user, qaSecret);
|
|
29
|
+
|
|
30
|
+
const result = await getMemberCMSId(userEmail);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
return { success: false, error: result.error };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
token,
|
|
38
|
+
memberCMSId: result.memberCMSId,
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('QA login error:', error);
|
|
42
|
+
return { error: 'Failed to generate session token' };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async function getMemberCMSId(userEmail) {
|
|
47
|
+
try {
|
|
48
|
+
const userValidation = await validateQAUser(userEmail);
|
|
49
|
+
if (userValidation.error) {
|
|
50
|
+
return { success: false, error: userValidation.error };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const member = await getMemberByEmail(userEmail);
|
|
54
|
+
|
|
55
|
+
if (!member) {
|
|
56
|
+
return { success: false, error: `No Member found in DB matching email: ${userEmail}` };
|
|
57
|
+
}
|
|
58
|
+
return { success: true, memberCMSId: member._id };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error getting member CMS ID:', error);
|
|
61
|
+
return { success: false, error: 'Failed to retrieve member data' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
loginQAMember,
|
|
67
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const { PAGES_PATHS } = require('../../public/consts');
|
|
2
|
+
//const { fetchAllItemsInParallel } = require('../cms-data-methods'); unused at host site
|
|
3
|
+
const { CONFIG_KEYS } = require('../consts');
|
|
4
|
+
const { getSiteConfigs } = require('../utils');
|
|
5
|
+
|
|
6
|
+
const { generateSEOTitle, stripHtmlTags, getMemberProfileData } = require('./utils');
|
|
7
|
+
|
|
8
|
+
const createRoutersHandlers = wixRouterMethods => {
|
|
9
|
+
const {
|
|
10
|
+
redirect,
|
|
11
|
+
ok,
|
|
12
|
+
notFound,
|
|
13
|
+
sendStatus,
|
|
14
|
+
WixRouterSitemapEntry: _WixRouterSitemapEntry,
|
|
15
|
+
} = wixRouterMethods; // These dependencies needs to be injected as they do not have an SDK equivalent for now
|
|
16
|
+
|
|
17
|
+
async function profileRouter(request) {
|
|
18
|
+
const slug = request.path[0];
|
|
19
|
+
if (!slug) {
|
|
20
|
+
return redirect(request.baseUrl);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const siteConfigs = await getSiteConfigs();
|
|
24
|
+
const siteAssociation = siteConfigs[CONFIG_KEYS.SITE_ASSOCIATION];
|
|
25
|
+
const defaultSEODescription = siteConfigs[CONFIG_KEYS.DEFAULT_PROFILE_SEO_DESCRIPTION];
|
|
26
|
+
const siteLogoUrl = siteConfigs[CONFIG_KEYS.SITE_LOGO_URL];
|
|
27
|
+
const defaultProfileImage = siteConfigs[CONFIG_KEYS.DEFAULT_PROFILE_IMAGE];
|
|
28
|
+
const profileData = await getMemberProfileData(slug, siteAssociation);
|
|
29
|
+
if (profileData && profileData.showWixUrl) {
|
|
30
|
+
const ogImage = profileData.profileImage || profileData.logoImage || siteLogoUrl;
|
|
31
|
+
const seoTitle = generateSEOTitle({
|
|
32
|
+
fullName: profileData.fullName,
|
|
33
|
+
areasOfPractices: profileData.areasOfPractices,
|
|
34
|
+
siteAssociation,
|
|
35
|
+
});
|
|
36
|
+
// Use stripped HTML from aboutService rich text content
|
|
37
|
+
let description = stripHtmlTags(profileData.aboutService) || defaultSEODescription;
|
|
38
|
+
|
|
39
|
+
// Limit description to 160 characters for optimal SEO
|
|
40
|
+
if (description.length > 160) {
|
|
41
|
+
description = description.substring(0, 157) + '...';
|
|
42
|
+
}
|
|
43
|
+
const profileUrl = `${request.baseUrl}/${PAGES_PATHS.PROFILE}/${profileData.url}`;
|
|
44
|
+
const isPrivateMember = profileData.isPrivateMember;
|
|
45
|
+
const seoData = {
|
|
46
|
+
title: seoTitle,
|
|
47
|
+
description: description,
|
|
48
|
+
noIndex: isPrivateMember,
|
|
49
|
+
metaTags: [
|
|
50
|
+
{
|
|
51
|
+
name: 'description',
|
|
52
|
+
content: description,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'keywords',
|
|
56
|
+
content:
|
|
57
|
+
`${profileData.fullName}, ${profileData.areasOfPractices ? profileData.areasOfPractices.slice(0, 3).join(', ') : ''}, ${siteAssociation}, ${profileData.city || ''}, ${profileData.state || ''}`
|
|
58
|
+
.replace(/,\s*,/g, ',')
|
|
59
|
+
.replace(/^,|,$/g, ''),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'author',
|
|
63
|
+
content: profileData.fullName,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'robots',
|
|
67
|
+
content: isPrivateMember ? 'noindex, nofollow' : 'index, follow',
|
|
68
|
+
},
|
|
69
|
+
// Open Graph tags
|
|
70
|
+
{
|
|
71
|
+
property: 'og:type',
|
|
72
|
+
content: 'profile',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
property: 'og:title',
|
|
76
|
+
content: seoTitle,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
property: 'og:description',
|
|
80
|
+
content: description,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
property: 'og:image',
|
|
84
|
+
content: ogImage,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
property: 'og:url',
|
|
88
|
+
content: profileUrl,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
property: 'og:site_name',
|
|
92
|
+
content: `${siteAssociation} Members`,
|
|
93
|
+
},
|
|
94
|
+
// Twitter Card tags
|
|
95
|
+
{
|
|
96
|
+
name: 'twitter:card',
|
|
97
|
+
content: 'summary_large_image',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'twitter:title',
|
|
101
|
+
content: seoTitle,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'twitter:description',
|
|
105
|
+
content: description,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'twitter:image',
|
|
109
|
+
content: ogImage,
|
|
110
|
+
},
|
|
111
|
+
// Additional SEO tags
|
|
112
|
+
{
|
|
113
|
+
name: 'geo.region',
|
|
114
|
+
content: profileData.state || '',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'geo.placename',
|
|
118
|
+
content: profileData.city || '',
|
|
119
|
+
},
|
|
120
|
+
].filter(tag => tag.content && tag.content.trim() !== ''), // Remove empty tags
|
|
121
|
+
};
|
|
122
|
+
return ok('profile', { ...profileData, defaultProfileImage }, seoData);
|
|
123
|
+
}
|
|
124
|
+
return notFound();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(error);
|
|
127
|
+
return sendStatus('500', 'Internal Server Error');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function profileSiteMap(_sitemapRequest) {
|
|
131
|
+
return [];
|
|
132
|
+
// Commented out - currently disabled in host site
|
|
133
|
+
// try {
|
|
134
|
+
// const membersQuery = wixData
|
|
135
|
+
// .query(COLLECTIONS.MEMBERS_DATA)
|
|
136
|
+
// .eq('showWixUrl', true)
|
|
137
|
+
// .isNotEmpty('url')
|
|
138
|
+
// .ne('action', 'drop')
|
|
139
|
+
// .fields('url', 'fullName');
|
|
140
|
+
|
|
141
|
+
// const allMembers = await fetchAllItemsInParallel(membersQuery);
|
|
142
|
+
|
|
143
|
+
// const batchSize = 1000;
|
|
144
|
+
// const sitemapEntries = [];
|
|
145
|
+
// const totalItems = allMembers.items.length;
|
|
146
|
+
|
|
147
|
+
// for (let i = 0; i < totalItems; i += batchSize) {
|
|
148
|
+
// const batch = allMembers.items.slice(i, i + batchSize);
|
|
149
|
+
// const batchEntries = batch.map(member => {
|
|
150
|
+
// const entry = new WixRouterSitemapEntry(member.fullName);
|
|
151
|
+
// entry.pageName = 'profile';
|
|
152
|
+
// entry.url = `${PAGES_PATHS.PROFILE}/${member.url}`;
|
|
153
|
+
// entry.title = member.fullName;
|
|
154
|
+
// entry.changeFrequency = 'monthly';
|
|
155
|
+
// entry.priority = 1.0;
|
|
156
|
+
// return entry;
|
|
157
|
+
// });
|
|
158
|
+
// sitemapEntries.push(...batchEntries);
|
|
159
|
+
// }
|
|
160
|
+
|
|
161
|
+
// return sitemapEntries;
|
|
162
|
+
// } catch (error) {
|
|
163
|
+
// console.error('Error generating profile sitemap:', error);
|
|
164
|
+
// return [];
|
|
165
|
+
// }
|
|
166
|
+
}
|
|
167
|
+
//Add other routers here
|
|
168
|
+
return {
|
|
169
|
+
profileRouter,
|
|
170
|
+
profileSiteMap,
|
|
171
|
+
//Add other routers here
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
createRoutersHandlers,
|
|
177
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const { getMainAddress } = require('../../public/Utils/sharedUtils');
|
|
2
|
+
const { getMemberBySlug } = require('../members-data-methods');
|
|
3
|
+
const {
|
|
4
|
+
getAddressesByStatus,
|
|
5
|
+
formatDateToMonthYear,
|
|
6
|
+
hasStudentMembership,
|
|
7
|
+
isPAC_STAFF,
|
|
8
|
+
} = require('../utils');
|
|
9
|
+
|
|
10
|
+
function generateSEOTitle({ fullName, areasOfPractices, siteAssociation }) {
|
|
11
|
+
return `${fullName}${
|
|
12
|
+
areasOfPractices && areasOfPractices.length > 0
|
|
13
|
+
? ` | ${areasOfPractices.slice(0, 3).join(', ')}`
|
|
14
|
+
: ''
|
|
15
|
+
} | ${siteAssociation} Member`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripHtmlTags(html) {
|
|
19
|
+
if (!html) return '';
|
|
20
|
+
// Remove HTML tags and decode HTML entities
|
|
21
|
+
return html
|
|
22
|
+
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
23
|
+
.replace(/ /g, ' ') // Replace non-breaking spaces
|
|
24
|
+
.replace(/&/g, '&') // Replace encoded ampersands
|
|
25
|
+
.replace(/</g, '<') // Replace encoded less than
|
|
26
|
+
.replace(/>/g, '>') // Replace encoded greater than
|
|
27
|
+
.replace(/"/g, '"') // Replace encoded quotes
|
|
28
|
+
.replace(/'/g, "'") // Replace encoded apostrophes
|
|
29
|
+
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
|
|
30
|
+
.trim(); // Remove leading/trailing whitespace
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shouldHaveStudentBadge(member, siteAssociation) {
|
|
34
|
+
return hasStudentMembership({
|
|
35
|
+
member,
|
|
36
|
+
checkAssociation: true,
|
|
37
|
+
siteAssociation,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function transformMemberToProfileData(member, siteAssociation) {
|
|
42
|
+
if (!member) {
|
|
43
|
+
throw new Error('member is required');
|
|
44
|
+
}
|
|
45
|
+
const addresses = member.addresses || [];
|
|
46
|
+
const mainAddress = getMainAddress(member.addressDisplayOption, addresses);
|
|
47
|
+
const licenceNo = member.licenses
|
|
48
|
+
?.map(val => val.license)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(', ');
|
|
51
|
+
const processedAddresses = getAddressesByStatus(member.addresses, member.addressDisplayOption);
|
|
52
|
+
|
|
53
|
+
const memberships = member.memberships || [];
|
|
54
|
+
const siteAssociationMembership = memberships.find(m => m.association === siteAssociation);
|
|
55
|
+
|
|
56
|
+
const areasOfPractices =
|
|
57
|
+
member.areasOfPractices
|
|
58
|
+
?.filter(item => typeof item === 'string' && item.trim().length > 0)
|
|
59
|
+
.map(item => item.trim())
|
|
60
|
+
.sort((a, b) =>
|
|
61
|
+
a.localeCompare(b, undefined, {
|
|
62
|
+
sensitivity: 'base',
|
|
63
|
+
numeric: true,
|
|
64
|
+
})
|
|
65
|
+
) || [];
|
|
66
|
+
return {
|
|
67
|
+
mainAddress,
|
|
68
|
+
testimonials: member.testimonial || [],
|
|
69
|
+
licenceNo,
|
|
70
|
+
processedAddresses,
|
|
71
|
+
memberSince:
|
|
72
|
+
(member.showABMP &&
|
|
73
|
+
siteAssociationMembership &&
|
|
74
|
+
formatDateToMonthYear(siteAssociationMembership?.membersince)) ||
|
|
75
|
+
'',
|
|
76
|
+
shouldHaveStudentBadge: shouldHaveStudentBadge(member, siteAssociation),
|
|
77
|
+
logoImage: member.logoImage,
|
|
78
|
+
fullName: member.fullName,
|
|
79
|
+
profileImage: member.profileImage,
|
|
80
|
+
showContactForm: member.showContactForm,
|
|
81
|
+
bookingUrl: member.bookingUrl,
|
|
82
|
+
aboutService: member.aboutService,
|
|
83
|
+
businessName: (member.showBusinessName && member.businessName) || '',
|
|
84
|
+
phone: member.toShowPhone || '',
|
|
85
|
+
areasOfPractices,
|
|
86
|
+
gallery: member.gallery,
|
|
87
|
+
bannerImages: member.bannerImages,
|
|
88
|
+
showWixUrl: member.showWixUrl,
|
|
89
|
+
_id: member._id,
|
|
90
|
+
url: member.url,
|
|
91
|
+
isPrivateMember: isPAC_STAFF(member),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const getMemberProfileData = async (slug, siteAssociation) => {
|
|
96
|
+
try {
|
|
97
|
+
const member = await getMemberBySlug({
|
|
98
|
+
slug,
|
|
99
|
+
excludeDropped: true,
|
|
100
|
+
excludeSearchedMember: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!member) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return transformMemberToProfileData(member, siteAssociation);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(error);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
generateSEOTitle,
|
|
116
|
+
stripHtmlTags,
|
|
117
|
+
getMemberProfileData,
|
|
118
|
+
};
|
package/backend/utils.js
CHANGED
|
@@ -3,17 +3,16 @@ const { secrets } = require('@wix/secrets');
|
|
|
3
3
|
const { site } = require('@wix/urls');
|
|
4
4
|
const { encode } = require('ngeohash');
|
|
5
5
|
|
|
6
|
-
const { COLLECTIONS } = require('../public/consts');
|
|
6
|
+
const { COLLECTIONS, ADDRESS_STATUS_TYPES } = require('../public/consts');
|
|
7
|
+
const { formatAddress, generateId } = require('../public/Utils/sharedUtils');
|
|
7
8
|
|
|
8
|
-
const { CONFIG_KEYS, GEO_HASH_PRECISION } = require('./consts');
|
|
9
|
+
const { CONFIG_KEYS, GEO_HASH_PRECISION, MEMBERSHIPS_TYPES } = require('./consts');
|
|
9
10
|
const { wixData } = require('./elevated-modules');
|
|
10
11
|
const elevatedGetSecretValue = auth.elevate(secrets.getSecretValue);
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Retrieves site configuration values from the database
|
|
14
|
-
* @param {
|
|
15
|
-
* - 'AUTOMATION_EMAIL_TRIGGER_ID' - Email template ID for triggered emails
|
|
16
|
-
* - 'SITE_ASSOCIATION' - Site association configuration
|
|
15
|
+
* @param {keyof typeof CONFIG_KEYS} [configKey] - The configuration key to retrieve
|
|
17
16
|
* @returns {Promise<any>} The configuration value for the specified key, or all configs if no key provided
|
|
18
17
|
* @example
|
|
19
18
|
* // Get specific config
|
|
@@ -59,18 +58,26 @@ function formatDateToMonthYear(dateString) {
|
|
|
59
58
|
return date.toLocaleDateString('en-US', options);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
* Check if member is a student
|
|
64
|
-
* @param {Object} member - The member object
|
|
65
|
-
* @returns {boolean} True if member has student membership
|
|
66
|
-
*/
|
|
67
|
-
function isStudent(member) {
|
|
61
|
+
function hasStudentMembership({ member, checkAssociation = false, siteAssociation = null }) {
|
|
68
62
|
const memberships = member?.memberships;
|
|
69
63
|
if (!Array.isArray(memberships)) return false;
|
|
70
64
|
|
|
71
|
-
return memberships.some(membership =>
|
|
65
|
+
return memberships.some(membership => {
|
|
66
|
+
const isStudent = membership.membertype === MEMBERSHIPS_TYPES.STUDENT;
|
|
67
|
+
const hasCorrectAssociation = !checkAssociation || membership.association === siteAssociation;
|
|
68
|
+
return isStudent && hasCorrectAssociation;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isStudent(member) {
|
|
73
|
+
return hasStudentMembership({ member, checkAssociation: false });
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
function isPAC_STAFF(member) {
|
|
77
|
+
return Boolean(
|
|
78
|
+
member?.memberships?.some(membership => membership.membertype === MEMBERSHIPS_TYPES.PAC_STAFF)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
74
81
|
/**
|
|
75
82
|
* Get address display options for member
|
|
76
83
|
* @param {Object} member - The member object
|
|
@@ -84,7 +91,22 @@ function getAddressDisplayOptions(member) {
|
|
|
84
91
|
}
|
|
85
92
|
return displayOptions;
|
|
86
93
|
}
|
|
87
|
-
|
|
94
|
+
function getAddressesByStatus(addresses = [], addressDisplayOption = []) {
|
|
95
|
+
const visible = addresses.filter(addr => addr.addressStatus !== ADDRESS_STATUS_TYPES.DONT_SHOW);
|
|
96
|
+
if (visible.length < 2) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const opts = Array.isArray(addressDisplayOption) ? addressDisplayOption : [];
|
|
100
|
+
const mainOpt = opts.find(o => o.isMain);
|
|
101
|
+
const mainKey = mainOpt ? mainOpt.key : visible[0].key; // fallback to the first visible if none marked
|
|
102
|
+
return visible
|
|
103
|
+
.filter(addr => addr?.key !== mainKey)
|
|
104
|
+
.map(addr => {
|
|
105
|
+
const addressString = formatAddress(addr);
|
|
106
|
+
return addressString ? { _id: generateId(), address: addressString } : null;
|
|
107
|
+
})
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
}
|
|
88
110
|
const queryAllItems = async query => {
|
|
89
111
|
console.log('start query');
|
|
90
112
|
let oldResults = await query.find();
|
|
@@ -176,9 +198,12 @@ module.exports = {
|
|
|
176
198
|
queryAllItems,
|
|
177
199
|
formatDateToMonthYear,
|
|
178
200
|
isStudent,
|
|
201
|
+
hasStudentMembership,
|
|
179
202
|
getAddressDisplayOptions,
|
|
180
203
|
getSecret,
|
|
181
204
|
getSiteBaseUrl,
|
|
182
205
|
encodeXml,
|
|
183
206
|
formatDateOnly,
|
|
207
|
+
getAddressesByStatus,
|
|
208
|
+
isPAC_STAFF,
|
|
184
209
|
};
|
package/package.json
CHANGED
package/pages/QAPage.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { location: wixLocationFrontend } = require('@wix/site-location');
|
|
2
|
+
const { authentication } = require('@wix/site-members');
|
|
3
|
+
const { local } = require('@wix/site-storage');
|
|
4
|
+
|
|
5
|
+
async function qaPageOnReady({ $w: _$w, loginQAMember }) {
|
|
6
|
+
try {
|
|
7
|
+
const { userEmail, secret, redirectTo, ...restQueryParams } = await wixLocationFrontend.query();
|
|
8
|
+
|
|
9
|
+
if (!userEmail || !secret) {
|
|
10
|
+
throw new Error('Missing required parameters: userEmail and/or secret');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = await loginQAMember(userEmail, secret);
|
|
14
|
+
|
|
15
|
+
if (!result.success || !result.token) {
|
|
16
|
+
throw new Error(result.error || 'Login failed');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await authentication.applySessionToken(result.token);
|
|
20
|
+
console.log('QA user logged in successfully');
|
|
21
|
+
|
|
22
|
+
await local.setItem('memberId', result.memberCMSId);
|
|
23
|
+
const queryParams = new URLSearchParams({ ...restQueryParams, token: result.memberCMSId });
|
|
24
|
+
const redirectUrl = redirectTo ? `/${redirectTo}?${queryParams.toString()}` : '/';
|
|
25
|
+
|
|
26
|
+
await wixLocationFrontend.to(redirectUrl);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('QA login failed:', error);
|
|
29
|
+
|
|
30
|
+
const qaTextElement = _$w('#qaText');
|
|
31
|
+
if (qaTextElement) {
|
|
32
|
+
qaTextElement.text = 'Login failed: ' + (error.message || 'Unknown error');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
qaPageOnReady,
|
|
39
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { window } = require('@wix/site-window');
|
|
2
|
+
|
|
3
|
+
function deleteConfirmOnReady({ $w: _$w }) {
|
|
4
|
+
_$w('#delete').onClick(() => {
|
|
5
|
+
window.lightbox.close({
|
|
6
|
+
toDelete: true,
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
_$w('#cancel').onClick(() => {
|
|
11
|
+
window.lightbox.close({
|
|
12
|
+
toDelete: false,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
deleteConfirmOnReady,
|
|
19
|
+
};
|
package/pages/index.js
CHANGED
package/public/consts.js
CHANGED
|
@@ -12,6 +12,7 @@ const COLLECTIONS = {
|
|
|
12
12
|
INTERESTS: 'interests',
|
|
13
13
|
STATE_CITY_MAP: 'City',
|
|
14
14
|
UPDATED_LOGIN_EMAILS: 'updatedLoginEmails',
|
|
15
|
+
QA_Users: 'QA_Users', //Make QA users configurable per site
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -95,8 +96,6 @@ const PAGES_PATHS = {
|
|
|
95
96
|
MEMBERS_FORM: 'directory-website-update',
|
|
96
97
|
};
|
|
97
98
|
|
|
98
|
-
const ABMP_MEMBERS_HOME_URL = 'https://www.abmp.com/members';
|
|
99
|
-
|
|
100
99
|
module.exports = {
|
|
101
100
|
REGEX,
|
|
102
101
|
COLLECTIONS,
|
|
@@ -109,5 +108,4 @@ module.exports = {
|
|
|
109
108
|
FREE_WEBSITE_TEXT_STATES,
|
|
110
109
|
DEFAULT_BUSINESS_NAME_TEXT,
|
|
111
110
|
PAGES_PATHS,
|
|
112
|
-
ABMP_MEMBERS_HOME_URL,
|
|
113
111
|
};
|
package/public/index.js
CHANGED
package/pages/SaveAlerts.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const { lightbox } = require('@wix/site-window');
|
|
2
|
-
|
|
3
|
-
function saveAlertsOnReady({ $w: _$w }) {
|
|
4
|
-
const receivedData = lightbox.getContext();
|
|
5
|
-
_$w('#closeButton').onClick(async () => await lightbox.close());
|
|
6
|
-
_$w('#cancelButton').onClick(async () => await lightbox.close());
|
|
7
|
-
_$w('#leaveButton').link = receivedData?.membersExternalPortalUrl;
|
|
8
|
-
_$w('#leaveButton').target = '_blank';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
module.exports = {
|
|
12
|
-
saveAlertsOnReady,
|
|
13
|
-
};
|