abmp-npm 2.0.72 → 2.0.74
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/.claude/worktrees/fix-save-button-disable/.husky/pre-commit +19 -0
- package/.claude/worktrees/fix-save-button-disable/.prettierignore +7 -0
- package/.claude/worktrees/fix-save-button-disable/.prettierrc.json +16 -0
- package/.claude/worktrees/fix-save-button-disable/README.md +1 -0
- package/.claude/worktrees/fix-save-button-disable/backend/README-member-contact-flows.md +136 -0
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/daily-pull-execution-check.test.js +124 -0
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/ensure-unique-urls-in-batch.test.js +129 -0
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/login-email-sync.test.js +49 -0
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/transient-retry-and-bulk-lookup.test.js +129 -0
- package/.claude/worktrees/fix-save-button-disable/backend/__tests__/url-uniqueness.test.js +194 -0
- package/.claude/worktrees/fix-save-button-disable/backend/automations-methods.js +13 -0
- package/.claude/worktrees/fix-save-button-disable/backend/cms-data-methods.js +251 -0
- package/.claude/worktrees/fix-save-button-disable/backend/consts.js +58 -0
- package/.claude/worktrees/fix-save-button-disable/backend/contacts-methods.js +67 -0
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/bulk-process-methods.js +222 -0
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/consts.js +39 -0
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/index.js +5 -0
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/process-member-methods.js +213 -0
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/schedule-methods.js +41 -0
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/sync-to-cms-methods.js +161 -0
- package/.claude/worktrees/fix-save-button-disable/backend/daily-pull/utils.js +151 -0
- package/.claude/worktrees/fix-save-button-disable/backend/data-hooks.js +29 -0
- package/.claude/worktrees/fix-save-button-disable/backend/dev-only-methods.js +58 -0
- package/.claude/worktrees/fix-save-button-disable/backend/elevated-modules.js +18 -0
- package/.claude/worktrees/fix-save-button-disable/backend/forms-methods.js +52 -0
- package/.claude/worktrees/fix-save-button-disable/backend/http-functions/httpFunctions.js +86 -0
- package/.claude/worktrees/fix-save-button-disable/backend/http-functions/index.js +3 -0
- package/.claude/worktrees/fix-save-button-disable/backend/http-functions/interests.js +35 -0
- package/.claude/worktrees/fix-save-button-disable/backend/index.js +19 -0
- package/.claude/worktrees/fix-save-button-disable/backend/jobs.js +158 -0
- package/.claude/worktrees/fix-save-button-disable/backend/login/generate-member-session-token.js +27 -0
- package/.claude/worktrees/fix-save-button-disable/backend/login/index.js +8 -0
- package/.claude/worktrees/fix-save-button-disable/backend/login/qa-login-methods.js +54 -0
- package/.claude/worktrees/fix-save-button-disable/backend/login/sso-methods.js +159 -0
- package/.claude/worktrees/fix-save-button-disable/backend/member-contact-orchestration.js +111 -0
- package/.claude/worktrees/fix-save-button-disable/backend/members-area-methods.js +88 -0
- package/.claude/worktrees/fix-save-button-disable/backend/members-data-methods.js +789 -0
- package/.claude/worktrees/fix-save-button-disable/backend/pac-api-methods.js +54 -0
- package/.claude/worktrees/fix-save-button-disable/backend/routers/index.js +3 -0
- package/.claude/worktrees/fix-save-button-disable/backend/routers/methods.js +191 -0
- package/.claude/worktrees/fix-save-button-disable/backend/routers/utils.js +128 -0
- package/.claude/worktrees/fix-save-button-disable/backend/search-filters-methods.js +124 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-primary-methods.js +256 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/address-visibility-methods.js +160 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/consts.js +34 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/daily-pull-check-methods.js +78 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/email-normalize-methods.js +134 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/index.js +10 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/migration-methods.js +46 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-configs.js +251 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-helpers-methods.js +433 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/tasks-process-methods.js +606 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-migration-methods.js +378 -0
- package/.claude/worktrees/fix-save-button-disable/backend/tasks/url-space-fix-methods.js +174 -0
- package/.claude/worktrees/fix-save-button-disable/backend/utils.js +277 -0
- package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/extract-duplicate-url-groups.js +201 -0
- package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-ids.js +159 -0
- package/.claude/worktrees/fix-save-button-disable/dev-only-scripts/find-duplicate-urls.js +201 -0
- package/.claude/worktrees/fix-save-button-disable/eslint.config.js +120 -0
- package/.claude/worktrees/fix-save-button-disable/index.js +5 -0
- package/.claude/worktrees/fix-save-button-disable/package-lock.json +12160 -0
- package/.claude/worktrees/fix-save-button-disable/package.json +60 -0
- package/.claude/worktrees/fix-save-button-disable/pages/ContactUs.js +129 -0
- package/.claude/worktrees/fix-save-button-disable/pages/Home.js +779 -0
- package/.claude/worktrees/fix-save-button-disable/pages/LearnMore.js +27 -0
- package/.claude/worktrees/fix-save-button-disable/pages/LoadingPage.js +29 -0
- package/.claude/worktrees/fix-save-button-disable/pages/Profile.js +354 -0
- package/.claude/worktrees/fix-save-button-disable/pages/QAPage.js +39 -0
- package/.claude/worktrees/fix-save-button-disable/pages/SaveAlerts.js +13 -0
- package/.claude/worktrees/fix-save-button-disable/pages/SelectBannerImages.js +46 -0
- package/.claude/worktrees/fix-save-button-disable/pages/deleteConfirm.js +19 -0
- package/.claude/worktrees/fix-save-button-disable/pages/index.js +12 -0
- package/.claude/worktrees/fix-save-button-disable/pages/personalDetails.js +2486 -0
- package/.claude/worktrees/fix-save-button-disable/public/Utils/homePage.js +798 -0
- package/.claude/worktrees/fix-save-button-disable/public/Utils/personalDetailsUtils.js +25 -0
- package/.claude/worktrees/fix-save-button-disable/public/Utils/sharedUtils.js +235 -0
- package/.claude/worktrees/fix-save-button-disable/public/consts.js +113 -0
- package/.claude/worktrees/fix-save-button-disable/public/index.js +5 -0
- package/.claude/worktrees/fix-save-button-disable/public/messages.js +16 -0
- package/.claude/worktrees/fix-save-button-disable/public/sso-auth-methods.js +43 -0
- package/backend/jobs.js +19 -0
- package/backend/tasks/address-visibility-methods.js +160 -0
- package/backend/tasks/consts.js +2 -0
- package/backend/tasks/tasks-configs.js +18 -0
- package/package.json +1 -1
- package/pages/personalDetails.js +33 -34
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// handle custom validation for url inputs
|
|
2
|
+
function handleOnCustomValidation(element) {
|
|
3
|
+
element.onCustomValidation((value, reject) => {
|
|
4
|
+
if (isNotValidUrl(value)) {
|
|
5
|
+
reject('Please enter a valid URL');
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// check if url is not valid using regex
|
|
11
|
+
function isNotValidUrl(url) {
|
|
12
|
+
// Empty URLs are considered valid (optional field)
|
|
13
|
+
if (!url) return false;
|
|
14
|
+
|
|
15
|
+
// URL must start with protocol OR www - handles all TLDs including multi-level and query params
|
|
16
|
+
const urlRegex =
|
|
17
|
+
/^(https?:\/\/|www\.)([da-z.-]+)\.([a-z.]{2,})([/\w .-]*)*(\?[&\w=.-]*)?(#[&\w=.-]*)?\/?$/;
|
|
18
|
+
|
|
19
|
+
return !urlRegex.test(url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
handleOnCustomValidation,
|
|
24
|
+
isNotValidUrl,
|
|
25
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const { ADDRESS_STATUS_TYPES, DEBOUNCE_DELAY } = require('../consts.js');
|
|
2
|
+
|
|
3
|
+
const checkAddressIsVisible = (addresses = []) => {
|
|
4
|
+
// ensure we always get an array
|
|
5
|
+
if (!Array.isArray(addresses)) return [];
|
|
6
|
+
|
|
7
|
+
return addresses.filter(
|
|
8
|
+
val => val.addressStatus === undefined || val.addressStatus !== ADDRESS_STATUS_TYPES.DONT_SHOW
|
|
9
|
+
);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//Formats an array of practice areas, showing as many as fit within 70 characters
|
|
13
|
+
function formatPracticeAreasForDisplay(areaOfPractices = []) {
|
|
14
|
+
// always return a string
|
|
15
|
+
if (!Array.isArray(areaOfPractices) || areaOfPractices.length === 0) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Filter out null/undefined/empty
|
|
20
|
+
const validAreas = areaOfPractices.filter(
|
|
21
|
+
area => area !== null && area !== undefined && area !== ''
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (validAreas.length === 0) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (validAreas.length === 1) {
|
|
29
|
+
return validAreas[0].length > 70 ? validAreas[0].substring(0, 67) + '...' : validAreas[0];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// build up to 70-char string
|
|
33
|
+
let current = '';
|
|
34
|
+
const visible = [];
|
|
35
|
+
for (const item of validAreas) {
|
|
36
|
+
const sep = visible.length ? ', ' : '';
|
|
37
|
+
const next = current + sep + item;
|
|
38
|
+
if (next.length > 70) break;
|
|
39
|
+
visible.push(item);
|
|
40
|
+
current = next;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// if nothing fit, at least show the first (truncated)
|
|
44
|
+
if (visible.length === 0) {
|
|
45
|
+
const first = validAreas[0];
|
|
46
|
+
return first.length > 67 ? first.substring(0, 67) + '...' : first;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const remaining = validAreas.length - visible.length;
|
|
50
|
+
return remaining > 0 ? `${visible.join(', ')}, +${remaining} Techniques` : visible.join(', ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function debouncedFunction({ func, debounceTimeout, timeoutType, args }) {
|
|
54
|
+
return new Promise(resolve => {
|
|
55
|
+
debounceTimeout[timeoutType] = setTimeout(async () => {
|
|
56
|
+
try {
|
|
57
|
+
const response = await func(args);
|
|
58
|
+
resolve({ success: true, response });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error updating results:', error);
|
|
61
|
+
resolve({ success: false, error });
|
|
62
|
+
}
|
|
63
|
+
}, DEBOUNCE_DELAY[timeoutType]);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isValidLocation = location => location.latitude && location.longitude;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {Array} addressDisplayOption
|
|
71
|
+
* @param {Array} addresses
|
|
72
|
+
* @param {Object|boolean} [options] - Optional. Pass { requireValidCoordinates: true } for home search/distance; omit or false for profile display.
|
|
73
|
+
*/
|
|
74
|
+
function findMainAddress(addressDisplayOption = [], addresses = [], options = {}) {
|
|
75
|
+
const requireValidCoordinates =
|
|
76
|
+
typeof options === 'boolean' ? options : Boolean(options?.requireValidCoordinates);
|
|
77
|
+
const optionsArr = Array.isArray(addressDisplayOption) ? addressDisplayOption : [];
|
|
78
|
+
const mainOpt = optionsArr.find(opt => opt.isMain);
|
|
79
|
+
if (mainOpt) {
|
|
80
|
+
const mainAddr = addresses.find(
|
|
81
|
+
addr =>
|
|
82
|
+
addr.key === mainOpt.key &&
|
|
83
|
+
addr.addressStatus !== ADDRESS_STATUS_TYPES.DONT_SHOW &&
|
|
84
|
+
(!requireValidCoordinates || isValidLocation(addr))
|
|
85
|
+
);
|
|
86
|
+
if (mainAddr) {
|
|
87
|
+
return mainAddr;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const visibleAddresses = addresses.filter(
|
|
91
|
+
addr =>
|
|
92
|
+
addr.addressStatus !== ADDRESS_STATUS_TYPES.DONT_SHOW &&
|
|
93
|
+
(!requireValidCoordinates || isValidLocation(addr))
|
|
94
|
+
);
|
|
95
|
+
if (visibleAddresses.length) {
|
|
96
|
+
return visibleAddresses[0];
|
|
97
|
+
}
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
function formatAddress(item) {
|
|
101
|
+
if (!item) return '';
|
|
102
|
+
let addressParts = [];
|
|
103
|
+
const limitedPostalCode = (item.postalcode && String(item.postalcode).slice(0, 5)) || ''; //show only 5 digits to not show full user address
|
|
104
|
+
const status = item.addressStatus;
|
|
105
|
+
switch (status) {
|
|
106
|
+
case ADDRESS_STATUS_TYPES.FULL_ADDRESS:
|
|
107
|
+
addressParts = [item.line1, item.line2, item.city, item.state, limitedPostalCode];
|
|
108
|
+
break;
|
|
109
|
+
case ADDRESS_STATUS_TYPES.STATE_CITY_ZIP:
|
|
110
|
+
addressParts = [item.city, item.state, limitedPostalCode];
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
if (status === ADDRESS_STATUS_TYPES.DONT_SHOW) return '';
|
|
114
|
+
// Legacy addresses may have no addressStatus; show city/state/zip by default
|
|
115
|
+
addressParts = [item.city, item.state, limitedPostalCode];
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
return addressParts.filter(Boolean).join(', ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {Array} addressDisplayOption
|
|
123
|
+
* @param {Array} addresses
|
|
124
|
+
* @param {Object|boolean} [options] - Optional. Pass { requireValidCoordinates: true } for home search/distance; omit or false for profile display.
|
|
125
|
+
*/
|
|
126
|
+
function getMainAddress(addressDisplayOption = [], addresses = [], options = {}) {
|
|
127
|
+
const mainAddr = findMainAddress(addressDisplayOption, addresses, options);
|
|
128
|
+
if (mainAddr) {
|
|
129
|
+
return formatAddress(mainAddr);
|
|
130
|
+
}
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
function shuffleArray(array) {
|
|
134
|
+
const shuffled = [...array];
|
|
135
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
136
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
137
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
138
|
+
}
|
|
139
|
+
return shuffled;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toRadians(degrees) {
|
|
143
|
+
return degrees * (Math.PI / 180);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function calculateDistance(location1, location2) {
|
|
147
|
+
if (!isValidLocation(location1) || !isValidLocation(location2)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Earth's radius in miles
|
|
152
|
+
const earthRadius = 3958.8;
|
|
153
|
+
|
|
154
|
+
// Convert latitude and longitude from degrees to radians
|
|
155
|
+
const latRad1 = toRadians(location1.latitude);
|
|
156
|
+
const lonRad1 = toRadians(location1.longitude);
|
|
157
|
+
const latRad2 = toRadians(location2.latitude);
|
|
158
|
+
const lonRad2 = toRadians(location2.longitude);
|
|
159
|
+
|
|
160
|
+
// Differences in coordinates
|
|
161
|
+
const latDiff = latRad2 - latRad1;
|
|
162
|
+
const lonDiff = lonRad2 - lonRad1;
|
|
163
|
+
|
|
164
|
+
// Haversine formula
|
|
165
|
+
const a =
|
|
166
|
+
Math.sin(latDiff / 2) * Math.sin(latDiff / 2) +
|
|
167
|
+
Math.cos(latRad1) * Math.cos(latRad2) * Math.sin(lonDiff / 2) * Math.sin(lonDiff / 2);
|
|
168
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
169
|
+
const distance = earthRadius * c;
|
|
170
|
+
|
|
171
|
+
return distance;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generate a unique ID
|
|
176
|
+
* @returns {string} Unique identifier
|
|
177
|
+
*/
|
|
178
|
+
function generateId() {
|
|
179
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isWixHostedImage(imageUrl) {
|
|
183
|
+
return (
|
|
184
|
+
imageUrl?.trim() &&
|
|
185
|
+
(imageUrl?.startsWith('wix:') || imageUrl?.startsWith('https://static.wixstatic.com'))
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Web URLs only: bare hostnames get https:// so they are not treated as site-relative paths. */
|
|
190
|
+
function normalizeExternalUrl(url) {
|
|
191
|
+
if (!url || typeof url !== 'string') return url;
|
|
192
|
+
const trimmed = url.trim();
|
|
193
|
+
if (!trimmed) return trimmed;
|
|
194
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
195
|
+
return `https://${trimmed}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Normalizes an email for comparison (lowercased + trimmed).
|
|
200
|
+
* Wix CRM treats emails as case-insensitive, so comparisons must too.
|
|
201
|
+
* @param {string} email
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
function normalizeEmail(email) {
|
|
205
|
+
return typeof email === 'string' ? email.trim().toLowerCase() : '';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Case-insensitive email equality. Two empty/missing emails are not considered a match.
|
|
210
|
+
* @param {string} a
|
|
211
|
+
* @param {string} b
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function emailsMatch(a, b) {
|
|
215
|
+
const normalizedA = normalizeEmail(a);
|
|
216
|
+
const normalizedB = normalizeEmail(b);
|
|
217
|
+
return Boolean(normalizedA) && normalizedA === normalizedB;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
checkAddressIsVisible,
|
|
222
|
+
formatPracticeAreasForDisplay,
|
|
223
|
+
debouncedFunction,
|
|
224
|
+
findMainAddress,
|
|
225
|
+
getMainAddress,
|
|
226
|
+
shuffleArray,
|
|
227
|
+
calculateDistance,
|
|
228
|
+
toRadians,
|
|
229
|
+
generateId,
|
|
230
|
+
formatAddress,
|
|
231
|
+
isWixHostedImage,
|
|
232
|
+
normalizeExternalUrl,
|
|
233
|
+
normalizeEmail,
|
|
234
|
+
emailsMatch,
|
|
235
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const REGEX = {
|
|
2
|
+
NAME: /^[a-zA-Z\s'-]{2,}$/,
|
|
3
|
+
MESSAGE: /^[A-Za-z0-9\s.,!?'"-]{2,}$/,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const COLLECTIONS = {
|
|
7
|
+
MEMBERS_DATA: 'MembersDataLatest',
|
|
8
|
+
CONTACT_US_SUBMISSIONS: 'contactUsSubmissions',
|
|
9
|
+
SITE_CONFIGS: 'SiteConfigs',
|
|
10
|
+
COMPILED_STATE_CITY_MAP: 'CompiledStateCityMap',
|
|
11
|
+
STATE: 'State',
|
|
12
|
+
INTERESTS: 'interests',
|
|
13
|
+
STATE_CITY_MAP: 'City',
|
|
14
|
+
UPDATED_LOGIN_EMAILS: 'updatedLoginEmails',
|
|
15
|
+
QA_USERS: 'QA_Users', //Make QA users configurable per site
|
|
16
|
+
BUTTON_CLICKS: 'ButtonClicks',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Address display status types
|
|
21
|
+
*/
|
|
22
|
+
const ADDRESS_STATUS_TYPES = {
|
|
23
|
+
FULL_ADDRESS: 'full_address',
|
|
24
|
+
STATE_CITY_ZIP: 'state_city_zip',
|
|
25
|
+
DONT_SHOW: 'dont_show',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_FILTER = {
|
|
29
|
+
skip: 0,
|
|
30
|
+
limit: 12,
|
|
31
|
+
practiceAreas: [],
|
|
32
|
+
practiceAreasSearch: '',
|
|
33
|
+
latitude: 0,
|
|
34
|
+
longitude: 0,
|
|
35
|
+
postalcode: null,
|
|
36
|
+
state: [],
|
|
37
|
+
stateSearch: '',
|
|
38
|
+
citySearch: '',
|
|
39
|
+
city: [],
|
|
40
|
+
searchText: null,
|
|
41
|
+
};
|
|
42
|
+
const DROPDOWN_OPTIONS = {
|
|
43
|
+
state: {
|
|
44
|
+
selectedOptions: [],
|
|
45
|
+
displayText: '',
|
|
46
|
+
},
|
|
47
|
+
city: {
|
|
48
|
+
selectedOptions: [],
|
|
49
|
+
displayText: '',
|
|
50
|
+
},
|
|
51
|
+
practiceAreas: {
|
|
52
|
+
selectedOptions: [],
|
|
53
|
+
displayText: '',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
const DEBOUNCE_DELAY = {
|
|
57
|
+
searchTimeout: 1000,
|
|
58
|
+
filterTimeout: 300,
|
|
59
|
+
zeroTimeout: 0,
|
|
60
|
+
};
|
|
61
|
+
const MEMBERS_FIELDS = {
|
|
62
|
+
_id: '_id',
|
|
63
|
+
profileImage: 'profileImage',
|
|
64
|
+
fullName: 'fullName',
|
|
65
|
+
addresses: 'addresses',
|
|
66
|
+
aboutService: 'aboutService',
|
|
67
|
+
showBookingUrl: 'showBookingUrl',
|
|
68
|
+
bookingUrl: 'bookingUrl',
|
|
69
|
+
areasOfPractices: 'areasOfPractices',
|
|
70
|
+
url: 'url',
|
|
71
|
+
phones: 'phones',
|
|
72
|
+
toShowPhone: 'toShowPhone',
|
|
73
|
+
showContactForm: 'showContactForm',
|
|
74
|
+
website: 'website',
|
|
75
|
+
showWixUrl: 'showWixUrl',
|
|
76
|
+
memberships: 'memberships',
|
|
77
|
+
showWebsite: 'showWebsite',
|
|
78
|
+
addressDisplayOption: 'addressDisplayOption',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const LIGHTBOX_NAMES = {
|
|
82
|
+
SAVE_ALERT: 'Save Alert',
|
|
83
|
+
LOGIN_ERROR_ALERT: 'loginError',
|
|
84
|
+
DELETE_CONFIRM: 'deleteConfirm',
|
|
85
|
+
SELECT_BANNER_IMAGES: 'Select Banner Images',
|
|
86
|
+
CONTACT_US: 'Contact Us',
|
|
87
|
+
MAIN_ADDRESS_ERROR: 'mainAddressError',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const FREE_WEBSITE_TEXT_STATES = {
|
|
91
|
+
ENABLED: 'This is the default and will auto-populate with the information entered on this page.',
|
|
92
|
+
DISABLED: 'To deactivate, please opt in via Personal Details.',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const DEFAULT_BUSINESS_NAME_TEXT = 'Business name not provided';
|
|
96
|
+
const PAGES_PATHS = {
|
|
97
|
+
PROFILE: 'profile',
|
|
98
|
+
MEMBERS_FORM: 'directory-website-update',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
REGEX,
|
|
103
|
+
COLLECTIONS,
|
|
104
|
+
ADDRESS_STATUS_TYPES,
|
|
105
|
+
DEFAULT_FILTER,
|
|
106
|
+
DROPDOWN_OPTIONS,
|
|
107
|
+
DEBOUNCE_DELAY,
|
|
108
|
+
MEMBERS_FIELDS,
|
|
109
|
+
LIGHTBOX_NAMES,
|
|
110
|
+
FREE_WEBSITE_TEXT_STATES,
|
|
111
|
+
DEFAULT_BUSINESS_NAME_TEXT,
|
|
112
|
+
PAGES_PATHS,
|
|
113
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const VALIDATION_MESSAGES = {
|
|
2
|
+
CONTACT_US: {
|
|
3
|
+
FIRST_NAME: 'Please enter a valid first name.',
|
|
4
|
+
LAST_NAME: 'Please enter a valid last name.',
|
|
5
|
+
MESSAGE: 'Please enter a valid message.',
|
|
6
|
+
PHONE: 'Please enter a valid US phone number.',
|
|
7
|
+
CAPTCHA: 'Please fix the highlighted fields before captcha verification.',
|
|
8
|
+
INVALID_FIELDS: 'Please fix the highlighted fields before submitting.',
|
|
9
|
+
SUBMISSION_FAILED: 'An error occurred. Please try again.',
|
|
10
|
+
SUBMISSION_SUCCESS: 'Contact submitted successfully',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
VALIDATION_MESSAGES,
|
|
16
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { location: wixLocationFrontend } = require('@wix/site-location');
|
|
2
|
+
const { authentication } = require('@wix/site-members');
|
|
3
|
+
const { local } = require('@wix/site-storage');
|
|
4
|
+
|
|
5
|
+
const { PAGES_PATHS } = require('./consts');
|
|
6
|
+
|
|
7
|
+
const checkAndLogin = async authenticateSSOToken => {
|
|
8
|
+
const query = await wixLocationFrontend.query();
|
|
9
|
+
const token = query['token']?.trim();
|
|
10
|
+
try {
|
|
11
|
+
if (token) {
|
|
12
|
+
const authObj = await authenticateSSOToken({ token });
|
|
13
|
+
console.log('authObj', authObj);
|
|
14
|
+
if (authObj.type == 'success') {
|
|
15
|
+
console.log('success');
|
|
16
|
+
await Promise.all([
|
|
17
|
+
authentication.applySessionToken(authObj?.sessionToken),
|
|
18
|
+
local.setItem('memberId', authObj.memberId),
|
|
19
|
+
]);
|
|
20
|
+
console.log('memberId', authObj.memberId);
|
|
21
|
+
const queryParams = {
|
|
22
|
+
...query,
|
|
23
|
+
token: authObj?.memberId,
|
|
24
|
+
};
|
|
25
|
+
const redirectTo = `${PAGES_PATHS.MEMBERS_FORM}?${new URLSearchParams(queryParams).toString()}`;
|
|
26
|
+
await wixLocationFrontend.to(`/${redirectTo}`);
|
|
27
|
+
} else {
|
|
28
|
+
console.error('Something went wrong while logging in');
|
|
29
|
+
throw new Error('Authentication failed - invalid response from server');
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
console.log('checkAndLogin: No token found');
|
|
33
|
+
throw new Error('No authentication token found in URL');
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error in checkAndLogin', error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
checkAndLogin,
|
|
43
|
+
};
|
package/backend/jobs.js
CHANGED
|
@@ -97,6 +97,24 @@ async function scheduleNormalizeMemberEmailsTask() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Schedules setting ALL addresses for ALL members that have any address to STATE_CITY_ZIP
|
|
102
|
+
* (show city/state/zip, hide the street). Manually triggered one-off maintenance task.
|
|
103
|
+
*/
|
|
104
|
+
async function scheduleSetAddressesToCityStateTask() {
|
|
105
|
+
try {
|
|
106
|
+
console.log('scheduleSetAddressesToCityState started!');
|
|
107
|
+
return await taskManager().schedule({
|
|
108
|
+
name: TASKS_NAMES.scheduleSetAddressesToCityState,
|
|
109
|
+
data: {},
|
|
110
|
+
type: 'scheduled',
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Failed to scheduleSetAddressesToCityState: ${error.message}`);
|
|
114
|
+
throw new Error(`Failed to scheduleSetAddressesToCityState: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
/**
|
|
101
119
|
* Runs the daily pull execution check (watchdog).
|
|
102
120
|
* @param {Object} [options]
|
|
@@ -135,5 +153,6 @@ module.exports = {
|
|
|
135
153
|
scheduleFixPrimaryAddressForMembersTask,
|
|
136
154
|
scheduleFixUrlsWithSpacesTask,
|
|
137
155
|
scheduleNormalizeMemberEmailsTask,
|
|
156
|
+
scheduleSetAddressesToCityStateTask,
|
|
138
157
|
runDailyPullExecutionCheck,
|
|
139
158
|
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const { taskManager } = require('psdev-task-manager');
|
|
2
|
+
|
|
3
|
+
const { COLLECTIONS, ADDRESS_STATUS_TYPES } = require('../../public/consts');
|
|
4
|
+
const { wixData } = require('../elevated-modules');
|
|
5
|
+
const { bulkSaveMembers, getMembersByIds } = require('../members-data-methods');
|
|
6
|
+
const { chunkArray, queryAllItems } = require('../utils');
|
|
7
|
+
|
|
8
|
+
const { TASKS_NAMES } = require('./consts');
|
|
9
|
+
|
|
10
|
+
const CHUNK_SIZE = 1000;
|
|
11
|
+
|
|
12
|
+
// Target visibility for every address: show city / state / zip, hide the street address.
|
|
13
|
+
const TARGET_STATUS = ADDRESS_STATUS_TYPES.STATE_CITY_ZIP;
|
|
14
|
+
|
|
15
|
+
const getMemberAddresses = member => (Array.isArray(member.addresses) ? member.addresses : []);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether a member has at least one address not already at the target status.
|
|
19
|
+
* @param {Object} member
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
const needsCityStateUpdate = member =>
|
|
23
|
+
getMemberAddresses(member).some(address => address && address.addressStatus !== TARGET_STATUS);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets every address on a member to STATE_CITY_ZIP (other address fields are preserved).
|
|
27
|
+
* @param {Object} member
|
|
28
|
+
* @returns {Array} the member's addresses with addressStatus set to STATE_CITY_ZIP
|
|
29
|
+
*/
|
|
30
|
+
const setAddressesToCityState = member =>
|
|
31
|
+
getMemberAddresses(member).map(address => ({
|
|
32
|
+
...address,
|
|
33
|
+
addressStatus: TARGET_STATUS,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Schedules tasks to set ALL addresses for ALL members to STATE_CITY_ZIP (show city/state/zip,
|
|
38
|
+
* hide the street). Manually triggered (not cron-wired).
|
|
39
|
+
*/
|
|
40
|
+
async function scheduleSetAddressesToCityState() {
|
|
41
|
+
console.log('=== Scheduling Set Addresses To City/State Tasks ===');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const membersQuery = await wixData
|
|
45
|
+
.query(COLLECTIONS.MEMBERS_DATA)
|
|
46
|
+
.isNotEmpty('addresses')
|
|
47
|
+
.limit(1000);
|
|
48
|
+
const members = await queryAllItems(membersQuery);
|
|
49
|
+
console.log(`Fetched ${members.length} members with addresses`);
|
|
50
|
+
|
|
51
|
+
// Only members that still have an address not at the target status need updating.
|
|
52
|
+
const memberIds = [
|
|
53
|
+
...new Set(
|
|
54
|
+
members
|
|
55
|
+
.filter(needsCityStateUpdate)
|
|
56
|
+
.map(member => Number(member.memberId))
|
|
57
|
+
.filter(memberId => Number.isFinite(memberId) && memberId > 0)
|
|
58
|
+
),
|
|
59
|
+
];
|
|
60
|
+
console.log(`Members needing a city/state update: ${memberIds.length}`);
|
|
61
|
+
|
|
62
|
+
if (memberIds.length === 0) {
|
|
63
|
+
console.log('No members need their addresses set to city/state');
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
message: 'No members need their addresses set to city/state',
|
|
67
|
+
totalMembers: 0,
|
|
68
|
+
tasksScheduled: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const chunks = chunkArray(memberIds, CHUNK_SIZE);
|
|
73
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
74
|
+
const chunk = chunks[i];
|
|
75
|
+
await taskManager().schedule({
|
|
76
|
+
name: TASKS_NAMES.setAddressesToCityStateChunk,
|
|
77
|
+
data: {
|
|
78
|
+
memberIds: chunk,
|
|
79
|
+
chunkIndex: i,
|
|
80
|
+
totalChunks: chunks.length,
|
|
81
|
+
},
|
|
82
|
+
type: 'scheduled',
|
|
83
|
+
});
|
|
84
|
+
console.log(`Scheduled task ${i + 1}/${chunks.length} (${chunk.length} members)`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = {
|
|
88
|
+
success: true,
|
|
89
|
+
message: `Scheduled ${chunks.length} tasks for ${memberIds.length} members`,
|
|
90
|
+
totalMembers: memberIds.length,
|
|
91
|
+
tasksScheduled: chunks.length,
|
|
92
|
+
};
|
|
93
|
+
console.log('=== Scheduling Complete ===');
|
|
94
|
+
console.log(JSON.stringify(result, null, 2));
|
|
95
|
+
return result;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error scheduling set-addresses-to-city-state:', error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Processes a chunk of members and sets every address to STATE_CITY_ZIP.
|
|
104
|
+
* @param {Object} data
|
|
105
|
+
* @param {Array<number|string>} data.memberIds
|
|
106
|
+
* @param {number} data.chunkIndex
|
|
107
|
+
* @param {number} data.totalChunks
|
|
108
|
+
*/
|
|
109
|
+
async function setAddressesToCityStateChunk(data) {
|
|
110
|
+
const { memberIds, chunkIndex, totalChunks } = data;
|
|
111
|
+
console.log(
|
|
112
|
+
`Processing set-city-state chunk ${chunkIndex + 1}/${totalChunks} (${memberIds.length} members)`
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const result = { successful: 0, failed: 0, skipped: 0, errors: [], failedIds: [] };
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const members = await getMembersByIds(memberIds);
|
|
119
|
+
console.log(`Loaded ${members.length} members for this chunk`);
|
|
120
|
+
|
|
121
|
+
const membersToUpdate = [];
|
|
122
|
+
members.forEach(member => {
|
|
123
|
+
// Skip members that have no address or are already all at the target status.
|
|
124
|
+
if (!needsCityStateUpdate(member)) {
|
|
125
|
+
result.skipped++;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
membersToUpdate.push({
|
|
129
|
+
...member,
|
|
130
|
+
addresses: setAddressesToCityState(member),
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (membersToUpdate.length === 0) {
|
|
135
|
+
console.log('No members need updating in this batch');
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await bulkSaveMembers(membersToUpdate);
|
|
141
|
+
result.successful += membersToUpdate.length;
|
|
142
|
+
console.log(`✅ Set addresses to city/state for ${membersToUpdate.length} members`);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('❌ Error bulk saving members:', error);
|
|
145
|
+
result.failed += membersToUpdate.length;
|
|
146
|
+
result.failedIds.push(...membersToUpdate.map(member => member.memberId));
|
|
147
|
+
result.errors.push({ error: error.message, memberCount: membersToUpdate.length });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`Error processing set-city-state chunk ${chunkIndex}:`, error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
scheduleSetAddressesToCityState,
|
|
159
|
+
setAddressesToCityStateChunk,
|
|
160
|
+
};
|
package/backend/tasks/consts.js
CHANGED
|
@@ -22,6 +22,8 @@ const TASKS_NAMES = {
|
|
|
22
22
|
fixPrimaryAddressChunk: 'fixPrimaryAddressChunk',
|
|
23
23
|
scheduleFixUrlsWithSpaces: 'scheduleFixUrlsWithSpaces',
|
|
24
24
|
fixUrlsWithSpacesChunk: 'fixUrlsWithSpacesChunk',
|
|
25
|
+
scheduleSetAddressesToCityState: 'scheduleSetAddressesToCityState',
|
|
26
|
+
setAddressesToCityStateChunk: 'setAddressesToCityStateChunk',
|
|
25
27
|
scheduleNormalizeMemberEmails: 'scheduleNormalizeMemberEmails',
|
|
26
28
|
normalizeMemberEmailsChunk: 'normalizeMemberEmailsChunk',
|
|
27
29
|
dailyPullExecutionCheck: 'dailyPullExecutionCheck',
|
|
@@ -8,6 +8,10 @@ const {
|
|
|
8
8
|
scheduleFixPrimaryAddressForMembers,
|
|
9
9
|
fixPrimaryAddressChunk,
|
|
10
10
|
} = require('./address-primary-methods');
|
|
11
|
+
const {
|
|
12
|
+
scheduleSetAddressesToCityState,
|
|
13
|
+
setAddressesToCityStateChunk,
|
|
14
|
+
} = require('./address-visibility-methods');
|
|
11
15
|
const { TASKS_NAMES } = require('./consts');
|
|
12
16
|
const { dailyPullExecutionCheck } = require('./daily-pull-check-methods');
|
|
13
17
|
const {
|
|
@@ -207,6 +211,20 @@ const TASKS = {
|
|
|
207
211
|
shouldSkipCheck: () => false,
|
|
208
212
|
estimatedDurationSec: 80,
|
|
209
213
|
},
|
|
214
|
+
[TASKS_NAMES.scheduleSetAddressesToCityState]: {
|
|
215
|
+
name: TASKS_NAMES.scheduleSetAddressesToCityState,
|
|
216
|
+
getIdentifier: () => 'SHOULD_NEVER_SKIP',
|
|
217
|
+
process: scheduleSetAddressesToCityState,
|
|
218
|
+
shouldSkipCheck: () => false,
|
|
219
|
+
estimatedDurationSec: 80,
|
|
220
|
+
},
|
|
221
|
+
[TASKS_NAMES.setAddressesToCityStateChunk]: {
|
|
222
|
+
name: TASKS_NAMES.setAddressesToCityStateChunk,
|
|
223
|
+
getIdentifier: task => task.data,
|
|
224
|
+
process: setAddressesToCityStateChunk,
|
|
225
|
+
shouldSkipCheck: () => false,
|
|
226
|
+
estimatedDurationSec: 80,
|
|
227
|
+
},
|
|
210
228
|
[TASKS_NAMES.scheduleNormalizeMemberEmails]: {
|
|
211
229
|
name: TASKS_NAMES.scheduleNormalizeMemberEmails,
|
|
212
230
|
getIdentifier: () => 'SHOULD_NEVER_SKIP',
|