diff --git a/src/actions/emailTemplateActions.js b/src/actions/emailTemplateActions.js
index c2d0e5af85..36421cca90 100644
--- a/src/actions/emailTemplateActions.js
+++ b/src/actions/emailTemplateActions.js
@@ -41,6 +41,8 @@ export const EMAIL_TEMPLATE_ACTIONS = {
CLEAR_CURRENT_TEMPLATE: 'CLEAR_CURRENT_TEMPLATE',
};
+const createEmailTemplateError = (message, details = {}) => Object.assign(new Error(message), details);
+
// Action Creators
// Fetch all email templates with pagination and sorting
@@ -268,7 +270,10 @@ export const previewEmailTemplate = (id, variables = {}) => async (dispatch, get
payload: { message: errorMessage, errors: errorDetails, missing: missingVariables },
});
- throw { message: errorMessage, errors: errorDetails, missing: missingVariables };
+ throw createEmailTemplateError(errorMessage, {
+ errors: errorDetails,
+ missing: missingVariables,
+ });
}
};
@@ -318,6 +323,6 @@ export const validateEmailTemplate = id => async (dispatch, getState) => {
payload: { message: errorMessage, errors: errorDetails },
});
- throw { message: errorMessage, errors: errorDetails };
+ throw createEmailTemplateError(errorMessage, { errors: errorDetails });
}
};
diff --git a/src/components/EmailManagement/EmailManagementShared.module.css b/src/components/EmailManagement/EmailManagementShared.module.css
index f9696805c1..876d00404a 100644
--- a/src/components/EmailManagement/EmailManagementShared.module.css
+++ b/src/components/EmailManagement/EmailManagementShared.module.css
@@ -468,7 +468,7 @@
}
.dark-mode .btn-outline-primary:hover {
- background-color: #4a9eff;
+ background-color: #0056b3;
color: white;
}
@@ -566,4 +566,4 @@
.stat-card:hover {
transform: none;
}
-}
\ No newline at end of file
+}
diff --git a/src/components/EmailManagement/email-sender/IntegratedEmailSender.jsx b/src/components/EmailManagement/email-sender/IntegratedEmailSender.jsx
index b88cd42a46..6e2e2975ef 100644
--- a/src/components/EmailManagement/email-sender/IntegratedEmailSender.jsx
+++ b/src/components/EmailManagement/email-sender/IntegratedEmailSender.jsx
@@ -112,6 +112,135 @@ const VariableRow = React.memo(
}
}, [onImageLoadStatusChange, variable?.name]);
+ const invalidImageUrlMessage = youtubeId
+ ? `The YouTube video "${youtubeId}" could not be loaded. Please verify the video exists and is publicly accessible.`
+ : 'The provided URL is not a valid image or YouTube video. Please provide a direct image URL (.jpg, .png, .gif) or a valid YouTube link.';
+ const defaultInputPlaceholder =
+ variable.type === 'number' ? 'Enter number' : `Enter ${variable.name.toLowerCase()}`;
+
+ let variableInput;
+
+ if (variable.type === 'textarea') {
+ variableInput = (
+ onVariableChange(variable.name, e.target.value)}
+ placeholder={`Enter ${variable.name.toLowerCase()}`}
+ invalid={!!error}
+ className="variable-input variable-textarea"
+ />
+ );
+ } else if (variable.type === 'image') {
+ variableInput = (
+
+
onImageSourceChange(variable.name, e.target.value)}
+ placeholder="Image URL or YouTube link"
+ invalid={!!error}
+ className="variable-input"
+ />
+
+ Supports: Direct image URLs (.jpg, .png, .gif, .webp, .svg), YouTube links
+ {extractedValue && (Auto-extracted) }
+
+ {(value || extractedValue) && (
+
+
Preview:
+
+ {!imgError ? (
+
+ ) : (
+
+
+
+
+
Invalid Image/Video URL
+
+ {invalidImageUrlMessage}
+
+
+
+
+ )}
+ {extractedValue && (
+
+
+ Extracted from:
+
+
+ {value}
+
+
+ )}
+
+
+ )}
+
+ );
+ } else if (variable.type === 'url') {
+ variableInput = (
+
+
onVariableChange(variable.name, e.target.value)}
+ placeholder="https://example.com"
+ invalid={!!error}
+ className="variable-input"
+ />
+ {value && (
+
+ )}
+
+ );
+ } else {
+ variableInput = (
+ onVariableChange(variable.name, e.target.value)}
+ placeholder={defaultInputPlaceholder}
+ invalid={!!error}
+ className="variable-input"
+ />
+ );
+ }
+
return (
@@ -126,122 +255,7 @@ const VariableRow = React.memo(
- {variable.type === 'textarea' ? (
- onVariableChange(variable.name, e.target.value)}
- placeholder={`Enter ${variable.name.toLowerCase()}`}
- invalid={!!error}
- className="variable-input variable-textarea"
- />
- ) : variable.type === 'image' ? (
-
-
onImageSourceChange(variable.name, e.target.value)}
- placeholder="Image URL or YouTube link"
- invalid={!!error}
- className="variable-input"
- />
-
- Supports: Direct image URLs (.jpg, .png, .gif, .webp, .svg), YouTube links
- {extractedValue && (Auto-extracted) }
-
- {(value || extractedValue) && (
-
-
Preview:
-
- {!imgError ? (
-
- ) : (
-
-
-
-
-
Invalid Image/Video URL
-
- {youtubeId
- ? `The YouTube video "${youtubeId}" could not be loaded. Please verify the video exists and is publicly accessible.`
- : 'The provided URL is not a valid image or YouTube video. Please provide a direct image URL (.jpg, .png, .gif) or a valid YouTube link.'}
-
-
-
-
- )}
- {extractedValue && (
-
-
- Extracted from:
-
-
- {value}
-
-
- )}
-
-
- )}
-
- ) : variable.type === 'url' ? (
-
-
onVariableChange(variable.name, e.target.value)}
- placeholder="https://example.com"
- invalid={!!error}
- className="variable-input"
- />
- {value && (
-
- )}
-
- ) : (
- onVariableChange(variable.name, e.target.value)}
- placeholder={
- variable.type === 'number' ? 'Enter number' : `Enter ${variable.name.toLowerCase()}`
- }
- invalid={!!error}
- className="variable-input"
- />
- )}
+ {variableInput}
{error && {error}
}
diff --git a/src/components/EmailManagement/email-sender/IntegratedEmailSender.module.css b/src/components/EmailManagement/email-sender/IntegratedEmailSender.module.css
index 63eec88961..c23d4e988f 100644
--- a/src/components/EmailManagement/email-sender/IntegratedEmailSender.module.css
+++ b/src/components/EmailManagement/email-sender/IntegratedEmailSender.module.css
@@ -96,53 +96,53 @@
/* Mode button variants */
.mode-buttons .btn-primary {
- background: #007bff;
- border-color: #007bff;
+ background: #0056b3;
+ border-color: #0056b3;
color: white;
}
.mode-buttons .btn-outline-primary {
background: white;
- color: #007bff;
- border-color: #007bff;
+ color: #0056b3;
+ border-color: #0056b3;
}
.mode-buttons .btn-outline-primary:hover {
- background: #007bff;
+ background: #0056b3;
color: white;
}
.mode-buttons .btn-success {
- background: #28a745;
- border-color: #28a745;
+ background: #1e7e34;
+ border-color: #1e7e34;
color: white;
}
.mode-buttons .btn-outline-success {
background: white;
- color: #28a745;
- border-color: #28a745;
+ color: #1e7e34;
+ border-color: #1e7e34;
}
.mode-buttons .btn-outline-success:hover {
- background: #28a745;
+ background: #1e7e34;
color: white;
}
.mode-buttons .btn-info {
- background: #17a2b8;
- border-color: #17a2b8;
+ background: #0f6674;
+ border-color: #0f6674;
color: white;
}
.mode-buttons .btn-outline-info {
background: white;
- color: #17a2b8;
- border-color: #17a2b8;
+ color: #0f6674;
+ border-color: #0f6674;
}
.mode-buttons .btn-outline-info:hover {
- background: #17a2b8;
+ background: #0f6674;
color: white;
}
@@ -906,9 +906,9 @@ body.dark-mode .alert-warning * {
}
.dark-mode .mode-buttons .btn-outline-primary:hover {
- background: #3182ce;
+ background: #0056b3;
color: white;
- border-color: #3182ce;
+ border-color: #0056b3;
}
.dark-mode .mode-buttons .btn-outline-success {
@@ -918,9 +918,9 @@ body.dark-mode .alert-warning * {
}
.dark-mode .mode-buttons .btn-outline-success:hover {
- background: #38a169;
+ background: #1e7e34;
color: white;
- border-color: #38a169;
+ border-color: #1e7e34;
}
.dark-mode .mode-buttons .btn-outline-info {
@@ -930,9 +930,9 @@ body.dark-mode .alert-warning * {
}
.dark-mode .mode-buttons .btn-outline-info:hover {
- background: #17a2b8;
+ background: #0f6674;
color: white;
- border-color: #17a2b8;
+ border-color: #0f6674;
}
.dark-mode .action-buttons .btn-outline-secondary {
@@ -1177,4 +1177,4 @@ body.dark-mode .alert-warning * {
.draft-notification .d-flex.gap-2 button {
flex: 1;
}
-}
\ No newline at end of file
+}
diff --git a/src/components/EmailManagement/email-sender/WeeklyUpdateComposer.jsx b/src/components/EmailManagement/email-sender/WeeklyUpdateComposer.jsx
index 5105b4b434..ef948da93f 100644
--- a/src/components/EmailManagement/email-sender/WeeklyUpdateComposer.jsx
+++ b/src/components/EmailManagement/email-sender/WeeklyUpdateComposer.jsx
@@ -275,6 +275,43 @@ const WeeklyUpdateComposer = ({ onClose }) => {
[extractYouTubeId],
);
+ const getYouTubeValidationError = useCallback(
+ url => {
+ if (!url.trim()) {
+ return 'YouTube link is required';
+ }
+
+ return extractYouTubeId(url) ? '' : 'Please enter a valid YouTube URL';
+ },
+ [extractYouTubeId],
+ );
+
+ const getRecipientValidationResult = useCallback(
+ recipientText => {
+ if (!recipientText.trim()) {
+ return { error: 'Please enter at least one recipient', recipientEmails: [] };
+ }
+
+ const recipientEmails = parseRecipients(recipientText);
+
+ if (recipientEmails.length === 0) {
+ return { error: 'Please enter at least one valid email address', recipientEmails };
+ }
+
+ const invalidEmails = recipientEmails.filter(email => !validateEmail(email));
+
+ if (invalidEmails.length > 0) {
+ return {
+ error: `Invalid email addresses: ${invalidEmails.join(', ')}`,
+ recipientEmails,
+ };
+ }
+
+ return { error: '', recipientEmails };
+ },
+ [parseRecipients, validateEmail],
+ );
+
// Validate form
const validateForm = useCallback(() => {
const errors = {};
@@ -287,10 +324,9 @@ const WeeklyUpdateComposer = ({ onClose }) => {
errors.introParagraph = 'Intro paragraph is required';
}
- if (!youtubeLink.trim()) {
- errors.youtubeLink = 'YouTube link is required';
- } else if (!extractYouTubeId(youtubeLink)) {
- errors.youtubeLink = 'Please enter a valid YouTube URL';
+ const youtubeError = getYouTubeValidationError(youtubeLink);
+ if (youtubeError) {
+ errors.youtubeLink = youtubeError;
}
if (!closingParagraph.trim()) {
@@ -299,19 +335,12 @@ const WeeklyUpdateComposer = ({ onClose }) => {
// Validate recipients for specific distribution
if (emailDistribution === 'specific') {
- if (!recipients.trim()) {
- errors.recipients = 'Please enter at least one recipient';
+ const { error, recipientEmails } = getRecipientValidationResult(recipients);
+
+ if (error) {
+ errors.recipients = error;
} else {
- const recipientEmails = parseRecipients(recipients);
- if (recipientEmails.length === 0) {
- errors.recipients = 'Please enter at least one valid email address';
- } else {
- const invalidEmails = recipientEmails.filter(email => !validateEmail(email));
- if (invalidEmails.length > 0) {
- errors.recipients = `Invalid email addresses: ${invalidEmails.join(', ')}`;
- }
- setRecipientList(recipientEmails);
- }
+ setRecipientList(recipientEmails);
}
}
@@ -324,9 +353,8 @@ const WeeklyUpdateComposer = ({ onClose }) => {
closingParagraph,
emailDistribution,
recipients,
- parseRecipients,
- validateEmail,
- extractYouTubeId,
+ getRecipientValidationResult,
+ getYouTubeValidationError,
]);
// Generate HTML email content - using CSS classes
diff --git a/src/components/EmailManagement/email-sender/validation.js b/src/components/EmailManagement/email-sender/validation.js
index 505cc048fc..25e31e3e62 100644
--- a/src/components/EmailManagement/email-sender/validation.js
+++ b/src/components/EmailManagement/email-sender/validation.js
@@ -58,102 +58,81 @@ function isValidImage(value, extractedValue) {
return false;
}
-export function validateTemplateVariables(template, variableValues) {
- const errors = {};
-
- if (!template || !Array.isArray(template.variables) || template.variables.length === 0) {
- return errors;
- }
-
- template.variables.forEach(variable => {
- if (!variable || !variable.name) return;
-
- const name = variable.name;
- const type = variable.type || 'text';
- const required = variable.required !== undefined ? !!variable.required : true;
-
- const rawValue = variableValues?.[name];
- const extracted = variableValues?.[`${name}_extracted`];
-
- // If not required and empty, skip validation
- const hasAnyValue = isNonEmptyString(rawValue) || isNonEmptyString(extracted);
- if (!required && !hasAnyValue) return;
-
- switch (type) {
- case 'image': {
- if (!isValidImage(rawValue, extracted)) {
- errors[name] = required
- ? `${name} is required (valid image URL or YouTube link)`
- : `Please enter a valid image URL or YouTube link`;
- }
- break;
- }
- case 'url': {
- if (!isValidUrl(rawValue)) {
- errors[name] = required ? `${name} is required (valid URL)` : `Please enter a valid URL`;
- }
- break;
- }
- case 'number': {
- if (!isValidNumber(rawValue)) {
- errors[name] = required ? `${name} is required (number)` : `Please enter a valid number`;
- }
- break;
- }
- case 'textarea':
- case 'text':
- default: {
- if (!isNonEmptyString(rawValue)) {
- errors[name] = required ? `${name} is required` : `Please enter a value`;
- }
- break;
- }
- }
- });
+const TEXT_VALIDATION_RULE = {
+ isValid: ({ rawValue }) => isNonEmptyString(rawValue),
+ requiredMessage: name => `${name} is required`,
+ optionalMessage: 'Please enter a value',
+};
- return errors;
-}
+const VARIABLE_VALIDATION_RULES = {
+ image: {
+ isValid: ({ rawValue, extracted }) => isValidImage(rawValue, extracted),
+ requiredMessage: name => `${name} is required (valid image URL or YouTube link)`,
+ optionalMessage: 'Please enter a valid image URL or YouTube link',
+ },
+ url: {
+ isValid: ({ rawValue }) => isValidUrl(rawValue),
+ requiredMessage: name => `${name} is required (valid URL)`,
+ optionalMessage: 'Please enter a valid URL',
+ },
+ number: {
+ isValid: ({ rawValue }) => isValidNumber(rawValue),
+ requiredMessage: name => `${name} is required (number)`,
+ optionalMessage: 'Please enter a valid number',
+ },
+ textarea: TEXT_VALIDATION_RULE,
+ text: TEXT_VALIDATION_RULE,
+};
-export function validateVariable(variable, variableValues) {
+function getVariableValidationContext(variable, variableValues) {
if (!variable || !variable.name) return null;
+
const name = variable.name;
- const type = variable.type || 'text';
const required = variable.required !== undefined ? !!variable.required : true;
-
const rawValue = variableValues?.[name];
const extracted = variableValues?.[`${name}_extracted`];
const hasAnyValue = isNonEmptyString(rawValue) || isNonEmptyString(extracted);
if (!required && !hasAnyValue) return null;
- switch (type) {
- case 'image':
- return isValidImage(rawValue, extracted)
- ? null
- : required
- ? `${name} is required (valid image URL or YouTube link)`
- : 'Please enter a valid image URL or YouTube link';
- case 'url':
- return isValidUrl(rawValue)
- ? null
- : required
- ? `${name} is required (valid URL)`
- : 'Please enter a valid URL';
- case 'number':
- return isValidNumber(rawValue)
- ? null
- : required
- ? `${name} is required (number)`
- : 'Please enter a valid number';
- case 'textarea':
- case 'text':
- default:
- return isNonEmptyString(rawValue)
- ? null
- : required
- ? `${name} is required`
- : 'Please enter a value';
+ return {
+ name,
+ type: variable.type || 'text',
+ required,
+ rawValue,
+ extracted,
+ };
+}
+
+function getVariableValidationError(context) {
+ const rule = VARIABLE_VALIDATION_RULES[context.type] || TEXT_VALIDATION_RULE;
+
+ if (rule.isValid(context)) return null;
+
+ return context.required ? rule.requiredMessage(context.name) : rule.optionalMessage;
+}
+
+export function validateTemplateVariables(template, variableValues) {
+ if (!template || !Array.isArray(template.variables) || template.variables.length === 0) {
+ return {};
}
+
+ return template.variables.reduce((errors, variable) => {
+ const context = getVariableValidationContext(variable, variableValues);
+ if (!context) return errors;
+
+ const error = getVariableValidationError(context);
+ if (error) {
+ errors[context.name] = error;
+ }
+
+ return errors;
+ }, {});
+}
+
+export function validateVariable(variable, variableValues) {
+ const context = getVariableValidationContext(variable, variableValues);
+ return context ? getVariableValidationError(context) : null;
}
export function extractImageForVariableIfNeeded(variable, variableValues) {