fix(cms): hardcode styled email templates
This commit is contained in:
@@ -158,10 +158,10 @@ DEFAULT_REPLY_TO=support@yourdomain.com
|
|||||||
# confirmation, event-reminder and application-acknowledgement e-mails
|
# confirmation, event-reminder and application-acknowledgement e-mails
|
||||||
# (src/lib/event-mail.ts, src/lib/application-mail.ts).
|
# (src/lib/event-mail.ts, src/lib/application-mail.ts).
|
||||||
#
|
#
|
||||||
# The subject and body of those e-mails are editable in the CMS (the
|
# Their bilingual copy is hardcoded in src/lib/email-template-defaults.ts.
|
||||||
# email-settings single type); the SHARP-ER / EU / partner logo header needs
|
# Every message uses the styled HTML layout in src/lib/event-mail.ts. The
|
||||||
# PUBLIC_URL (above) to build absolute image URLs - without it the mail falls
|
# SHARP-ER / EU / partner logos need PUBLIC_URL (above) to build absolute image
|
||||||
# back to a text wordmark.
|
# URLs - without it the mail falls back to a text wordmark.
|
||||||
|
|
||||||
# Production only - switching the upload provider to S3. Uncomment these after
|
# Production only - switching the upload provider to S3. Uncomment these after
|
||||||
# adding @strapi/provider-upload-aws-s3 and editing the upload block in
|
# adding @strapi/provider-upload-aws-s3 and editing the upload block in
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"kind": "singleType",
|
|
||||||
"collectionName": "email_settings",
|
|
||||||
"info": {
|
|
||||||
"singularName": "email-settings",
|
|
||||||
"pluralName": "email-settings-list",
|
|
||||||
"displayName": "Email Settings",
|
|
||||||
"description": "The outbound email templates (registration confirmation, waitlist, reminder, application acknowledgement). Markdown bodies become HTML at send time. The values seeded here on first boot are also the defaults the mails fall back to when the row is missing. Read by the mail jobs only; not exposed over the public API."
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"draftAndPublish": false
|
|
||||||
},
|
|
||||||
"pluginOptions": {
|
|
||||||
"i18n": {
|
|
||||||
"localized": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"attributes": {
|
|
||||||
"templates": {
|
|
||||||
"type": "component",
|
|
||||||
"repeatable": true,
|
|
||||||
"component": "email.template",
|
|
||||||
"required": true,
|
|
||||||
"pluginOptions": {
|
|
||||||
"i18n": {
|
|
||||||
"localized": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import type { Core } from '@strapi/strapi';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DEFAULT_EMAIL_TEMPLATES,
|
|
||||||
EMAIL_TEMPLATE_KEYS,
|
|
||||||
type EmailTemplateKey,
|
|
||||||
} from '../lib/email-template-defaults';
|
|
||||||
import { LOG_PREFIX } from './constants';
|
|
||||||
|
|
||||||
const EMAIL_SETTINGS_UID = 'api::email-settings.email-settings';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `email.template` component payload for one locale of one template.
|
|
||||||
* `enabled` stays on so the shipped wording is the live configuration, keeping
|
|
||||||
* code defaults and CMS state in agreement on a fresh database.
|
|
||||||
*/
|
|
||||||
const templateRow = (key: EmailTemplateKey, locale: 'en' | 'ar') => ({
|
|
||||||
key,
|
|
||||||
enabled: true,
|
|
||||||
subject: DEFAULT_EMAIL_TEMPLATES[key][locale].subject,
|
|
||||||
body: DEFAULT_EMAIL_TEMPLATES[key][locale].body,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the `email-settings` single type if the database does not have one
|
|
||||||
* yet, so the four email templates exist and are editable in the admin panel
|
|
||||||
* after the very first boot — carrying the shipped default copy. Never
|
|
||||||
* overwrites an existing row, so editor customisation survives every restart.
|
|
||||||
*/
|
|
||||||
export const ensureEmailSettings = async (strapi: Core.Strapi): Promise<void> => {
|
|
||||||
const existing = await strapi
|
|
||||||
.documents(EMAIL_SETTINGS_UID)
|
|
||||||
.findFirst({});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templatesFor = (locale: 'en' | 'ar') =>
|
|
||||||
EMAIL_TEMPLATE_KEYS.map((key) => templateRow(key, locale));
|
|
||||||
|
|
||||||
/* Arabic first — it is the default locale and the one a single type is
|
|
||||||
created on — then the English translation is written onto the same
|
|
||||||
document, which is how Strapi 5 models one document in two locales. */
|
|
||||||
const created = await strapi.documents(EMAIL_SETTINGS_UID).create({
|
|
||||||
locale: 'ar',
|
|
||||||
data: { templates: templatesFor('ar') },
|
|
||||||
});
|
|
||||||
await strapi.documents(EMAIL_SETTINGS_UID).update({
|
|
||||||
documentId: created.documentId,
|
|
||||||
locale: 'en',
|
|
||||||
data: { templates: templatesFor('en') },
|
|
||||||
});
|
|
||||||
|
|
||||||
strapi.log.info(
|
|
||||||
`${LOG_PREFIX} created email-settings single type with the four default email templates.`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,7 +2,6 @@ import type { Core } from '@strapi/strapi';
|
|||||||
|
|
||||||
import { revokePublicAuthActions } from './auth-permissions';
|
import { revokePublicAuthActions } from './auth-permissions';
|
||||||
import { LOG_PREFIX } from './constants';
|
import { LOG_PREFIX } from './constants';
|
||||||
import { ensureEmailSettings } from './email-settings';
|
|
||||||
import { ensureLocales } from './locales';
|
import { ensureLocales } from './locales';
|
||||||
import { ensurePublicPermissions } from './public-permissions';
|
import { ensurePublicPermissions } from './public-permissions';
|
||||||
import { ensurePartnerReviewPermissions } from './partner-review-permissions';
|
import { ensurePartnerReviewPermissions } from './partner-review-permissions';
|
||||||
@@ -11,7 +10,6 @@ import { ensurePartnerRelationLabels } from './partner-relation-labels';
|
|||||||
import { ensureReminderSettings } from './reminder-settings';
|
import { ensureReminderSettings } from './reminder-settings';
|
||||||
|
|
||||||
export { revokePublicAuthActions } from './auth-permissions';
|
export { revokePublicAuthActions } from './auth-permissions';
|
||||||
export { ensureEmailSettings } from './email-settings';
|
|
||||||
export { ensureLocales } from './locales';
|
export { ensureLocales } from './locales';
|
||||||
export { ensurePublicPermissions } from './public-permissions';
|
export { ensurePublicPermissions } from './public-permissions';
|
||||||
export { ensureReminderSettings } from './reminder-settings';
|
export { ensureReminderSettings } from './reminder-settings';
|
||||||
@@ -34,7 +32,6 @@ export const runBootstrap = async ({ strapi }: { strapi: Core.Strapi }): Promise
|
|||||||
await ensurePublicPermissions(strapi);
|
await ensurePublicPermissions(strapi);
|
||||||
await revokePublicAuthActions(strapi);
|
await revokePublicAuthActions(strapi);
|
||||||
await ensureReminderSettings(strapi);
|
await ensureReminderSettings(strapi);
|
||||||
await ensureEmailSettings(strapi);
|
|
||||||
await ensurePartnerReviewPermissions(strapi);
|
await ensurePartnerReviewPermissions(strapi);
|
||||||
await ensurePartnerReviewLayouts(strapi);
|
await ensurePartnerReviewLayouts(strapi);
|
||||||
await ensurePartnerRelationLabels(strapi);
|
await ensurePartnerRelationLabels(strapi);
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"collectionName": "components_email_templates",
|
|
||||||
"info": {
|
|
||||||
"displayName": "Email Template",
|
|
||||||
"icon": "envelop",
|
|
||||||
"description": "One outbound email template: a subject plus a markdown body. The body is converted to HTML at send time. Placeholders: {event}, {date} and {call}. A placeholder whose value is missing collapses together with the word before it, so \"for {call}\" (and \"على {call}\") reads cleanly when there is no call."
|
|
||||||
},
|
|
||||||
"options": {},
|
|
||||||
"attributes": {
|
|
||||||
"key": {
|
|
||||||
"type": "enumeration",
|
|
||||||
"enum": [
|
|
||||||
"confirmation",
|
|
||||||
"waitlist",
|
|
||||||
"event-reminder",
|
|
||||||
"application-acknowledgement"
|
|
||||||
],
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Off reverts this email to the shipped default copy."
|
|
||||||
},
|
|
||||||
"subject": {
|
|
||||||
"type": "string",
|
|
||||||
"required": true,
|
|
||||||
"maxLength": 250
|
|
||||||
},
|
|
||||||
"body": {
|
|
||||||
"type": "richtext",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,19 +11,18 @@
|
|||||||
* retried by nothing today but is visible in the admin panel as a null.
|
* retried by nothing today but is visible in the admin panel as a null.
|
||||||
*
|
*
|
||||||
* The (Get Involved) section form can submit an application with no
|
* The (Get Involved) section form can submit an application with no
|
||||||
* opportunity relation. The mail then drops the call name and stays a plain
|
* opportunity relation. The mail then drops the call name and uses the
|
||||||
* acknowledgement, so the promise holds for both entry points.
|
* no-call acknowledgement, so the promise holds for both entry points.
|
||||||
*
|
*
|
||||||
* COPY IS CMS-DRIVEN like the event mails: subject and body come from the
|
* Copy is hardcoded in `email-template-defaults.ts`, and this message uses the
|
||||||
* `email-settings` single type's `application-acknowledgement` template, whose
|
* same styled HTML layout as the event emails. The call name fills the `{call}`
|
||||||
* shipped default is the approved wording. The call name fills the `{call}`
|
|
||||||
* placeholder; when there is no call name the placeholder and its preceding
|
* placeholder; when there is no call name the placeholder and its preceding
|
||||||
* word collapse, leaving the no-call variant automatically.
|
* word collapse, leaving the no-call variant automatically.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Core } from '@strapi/strapi';
|
import type { Core } from '@strapi/strapi';
|
||||||
|
|
||||||
import { resolveEmailTemplate } from './email-templates';
|
import { DEFAULT_EMAIL_TEMPLATES } from './email-template-defaults';
|
||||||
import { buildBilingualMail, trySendMail } from './event-mail';
|
import { buildBilingualMail, trySendMail } from './event-mail';
|
||||||
|
|
||||||
const OPPORTUNITY_UID = 'api::opportunity.opportunity';
|
const OPPORTUNITY_UID = 'api::opportunity.opportunity';
|
||||||
@@ -85,10 +84,8 @@ export async function sendApplicationAcknowledgement({
|
|||||||
opportunityReference: OpportunityReference;
|
opportunityReference: OpportunityReference;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [opportunity, template] = await Promise.all([
|
const opportunity = await fetchBilingualOpportunity(strapi, opportunityReference);
|
||||||
fetchBilingualOpportunity(strapi, opportunityReference),
|
const template = DEFAULT_EMAIL_TEMPLATES['application-acknowledgement'];
|
||||||
resolveEmailTemplate(strapi, 'application-acknowledgement'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const callEn = opportunity.en?.title || undefined;
|
const callEn = opportunity.en?.title || undefined;
|
||||||
const callAr = opportunity.ar?.title || opportunity.en?.title || undefined;
|
const callAr = opportunity.ar?.title || opportunity.en?.title || undefined;
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* email-template-defaults — the shipped subject and body for every email the
|
* email-template-defaults — the hardcoded subject and body for every email
|
||||||
* CMS sends.
|
* the CMS sends.
|
||||||
*
|
*
|
||||||
* These are the "default for each email template": first-boot writes them into
|
* This is the source of truth for outbound email copy. Changing a template
|
||||||
* the `email-settings` single type (src/bootstrap/email-settings.ts) so editors
|
* requires a code change and review; the CMS does not store editable copies.
|
||||||
* start from a working, editable baseline, and they remain the fallback in code
|
|
||||||
* when the single type is missing, unreadable or a template is switched off —
|
|
||||||
* which is what keeps the mails sending even if nothing was ever configured.
|
|
||||||
*
|
*
|
||||||
* The copy is the wording the mail modules hard-coded before this file existed,
|
* The copy is the wording the mail modules hard-coded before this file existed,
|
||||||
* which itself mirrors the approved web copy (apps/web i18n messages and
|
* which itself mirrors the approved web copy (apps/web i18n messages and
|
||||||
@@ -37,8 +34,8 @@ export type LocalisedEmailTemplate = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The two approved acknowledgement wordings, kept for their exact strings:
|
* The two approved acknowledgement wordings, kept for their exact strings:
|
||||||
* the with-call variant is the shipped default and the no-call variant is what
|
* the no-call variant is what placeholder collapse produces when there is no
|
||||||
* the placeholder collapse reproduces when there is no opportunity.
|
* opportunity.
|
||||||
*/
|
*/
|
||||||
const ACKNOWLEDGEMENT_WITH_CALL = {
|
const ACKNOWLEDGEMENT_WITH_CALL = {
|
||||||
ar: 'شكرًا لك — استلمنا طلبك على {call}. سيراجع فريقنا طلبك ويتواصل معك عبر البريد الإلكتروني.',
|
ar: 'شكرًا لك — استلمنا طلبك على {call}. سيراجع فريقنا طلبك ويتواصل معك عبر البريد الإلكتروني.',
|
||||||
@@ -46,8 +43,7 @@ const ACKNOWLEDGEMENT_WITH_CALL = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The plain acknowledgement (no call), reproduced verbatim so editors can
|
* The plain acknowledgement (no call), reproduced verbatim for reference.
|
||||||
* compare against the collapsed form when the placeholder is empty.
|
|
||||||
*/
|
*/
|
||||||
const ACKNOWLEDGEMENT_WITHOUT_CALL = {
|
const ACKNOWLEDGEMENT_WITHOUT_CALL = {
|
||||||
ar: 'شكرًا لك — استلمنا طلبك. سيراجع فريقنا طلبك ويتواصل معك عبر البريد الإلكتروني.',
|
ar: 'شكرًا لك — استلمنا طلبك. سيراجع فريقنا طلبك ويتواصل معك عبر البريد الإلكتروني.',
|
||||||
|
|||||||
@@ -1,53 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* email-templates — resolve hardcoded copy and shared placeholders for the
|
* email-templates — shared placeholder interpolation for hardcoded outbound
|
||||||
* outbound mails.
|
* email templates. Subjects and bodies live in email-template-defaults.ts.
|
||||||
*
|
|
||||||
* The subjects and markdown bodies live in email-template-defaults.ts. This
|
|
||||||
* resolver returns those values directly; the CMS has no runtime copy source.
|
|
||||||
*
|
|
||||||
* resolveEmailTemplate the subject/body pair for a key, per locale, each
|
|
||||||
* locale falling back to its own code default when
|
|
||||||
* the row is missing, switched off or unreadable.
|
|
||||||
* fillPlaceholders the {event} / {date} / {call} substitution, shared
|
|
||||||
* by both mail modules. An empty value removes the
|
|
||||||
* placeholder together with the word before it, so
|
|
||||||
* "for {call}" / "على {call}" collapse cleanly when
|
|
||||||
* there is no call (see the component description).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Core } from '@strapi/strapi';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DEFAULT_EMAIL_TEMPLATES,
|
|
||||||
type EmailTemplateKey,
|
|
||||||
type LocalisedEmailTemplate,
|
|
||||||
} from './email-template-defaults';
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
TEMPLATES
|
|
||||||
------------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
/** The hardcoded per-locale subject/body for one template key. */
|
|
||||||
export type ResolvedEmailTemplate = {
|
|
||||||
ar: LocalisedEmailTemplate;
|
|
||||||
en: LocalisedEmailTemplate;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The subject and body for one template key, per locale. The retained Strapi
|
|
||||||
* argument keeps callers stable while all senders move to the static source.
|
|
||||||
*/
|
|
||||||
export function resolveEmailTemplate(
|
|
||||||
_strapi: Core.Strapi,
|
|
||||||
key: EmailTemplateKey
|
|
||||||
): ResolvedEmailTemplate {
|
|
||||||
return DEFAULT_EMAIL_TEMPLATES[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
PLACEHOLDERS
|
|
||||||
------------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
/** Literal values for {event} / {date} / {call}, per locale, unescaped. */
|
/** Literal values for {event} / {date} / {call}, per locale, unescaped. */
|
||||||
export type PlaceholderValues = Readonly<Record<string, string>>;
|
export type PlaceholderValues = Readonly<Record<string, string>>;
|
||||||
|
|
||||||
|
|||||||
@@ -14,23 +14,16 @@
|
|||||||
* The Arabic block always comes first and is marked dir="rtl"; the English
|
* The Arabic block always comes first and is marked dir="rtl"; the English
|
||||||
* block follows inside the same message. Subjects are bilingual too.
|
* block follows inside the same message. Subjects are bilingual too.
|
||||||
*
|
*
|
||||||
* COPY IS CMS-DRIVEN. The subject and body for each mail come from the
|
* COPY IS HARDCODED in lib/email-template-defaults.ts. This module owns the
|
||||||
* `email-settings` single type — markdown bodies, converted to HTML at send
|
* shared styled HTML layout, event lookups, the {event} / {date} placeholders,
|
||||||
* time (see lib/markdown.ts) — with the shipped approved copy as the fallback
|
* venue or join-online line, and EU/partner branding.
|
||||||
* (see lib/email-template-defaults.ts). This module owns the chrome around
|
|
||||||
* that copy: the event lookups, the {event} / {date} placeholders, the venue
|
|
||||||
* or join-online line, and the logo header/footer (SHARP-ER logo, EU badge
|
|
||||||
* and the consortium partner logos, read from `site-settings`).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Core } from '@strapi/strapi';
|
import type { Core } from '@strapi/strapi';
|
||||||
|
|
||||||
import { buildEventCalendar } from './event-calendar';
|
import { buildEventCalendar } from './event-calendar';
|
||||||
import {
|
import { DEFAULT_EMAIL_TEMPLATES } from './email-template-defaults';
|
||||||
resolveEmailTemplate,
|
import { fillPlaceholders, type PlaceholderValues } from './email-templates';
|
||||||
fillPlaceholders,
|
|
||||||
type PlaceholderValues,
|
|
||||||
} from './email-templates';
|
|
||||||
import { mdToHtml, mdToText, escapeHtml } from './markdown';
|
import { mdToHtml, mdToText, escapeHtml } from './markdown';
|
||||||
|
|
||||||
const EVENT_UID = 'api::event.event';
|
const EVENT_UID = 'api::event.event';
|
||||||
@@ -401,27 +394,46 @@ export function buildBilingualMail(opts: {
|
|||||||
Object.entries(values).map(([name, value]) => [name, escapeHtml(value)])
|
Object.entries(values).map(([name, value]) => [name, escapeHtml(value)])
|
||||||
);
|
);
|
||||||
|
|
||||||
const html = `
|
const subject = `${fillPlaceholders(subjectAr, varsAr)} | ${fillPlaceholders(subjectEn, varsEn)}`;
|
||||||
|
|
||||||
|
const html = `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${escapeHtml(subject)}</title>
|
||||||
${renderJsonLd(jsonLd)}
|
${renderJsonLd(jsonLd)}
|
||||||
${renderBrandHeader()}
|
</head>
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${BASE_FONT}"><tr><td style="padding:0; line-height:1.6; color:#222;">
|
<body style="margin:0; padding:0; background-color:#f2f5f3; ${BASE_FONT}">
|
||||||
<div dir="rtl" lang="ar">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" bgcolor="#f2f5f3" style="width:100%; border-collapse:collapse; background-color:#f2f5f3;">
|
||||||
${fillPlaceholders(mdToHtml(bodyAr), escaped(varsAr))}
|
<tr><td align="center" style="padding:32px 12px;">
|
||||||
${locationHtml(locationLine?.ar)}
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" bgcolor="#ffffff" style="width:100%; max-width:640px; border:1px solid #dfe6e1; border-top:4px solid #356b57; border-radius:8px; background-color:#ffffff; border-collapse:separate; border-spacing:0;">
|
||||||
</div>
|
<tr><td style="padding:28px 32px 24px; ${BASE_FONT}">
|
||||||
<hr style="border:none; border-top:1px solid #ddd; margin:24px 0;" />
|
${renderBrandHeader()}
|
||||||
<div dir="ltr" lang="en">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${BASE_FONT}"><tr><td style="padding:0; line-height:1.7; color:#25312b; font-size:16px;">
|
||||||
${fillPlaceholders(mdToHtml(bodyEn), escaped(varsEn))}
|
<div dir="rtl" lang="ar" style="direction:rtl; text-align:right;">
|
||||||
${locationHtml(locationLine?.en)}
|
${fillPlaceholders(mdToHtml(bodyAr), escaped(varsAr))}
|
||||||
</div>
|
${locationHtml(locationLine?.ar)}
|
||||||
</td></tr></table>
|
</div>
|
||||||
<p style="margin-top:32px; font-size:12px; line-height:1.5; color:#666; ${BASE_FONT}">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:24px 0;"><tr><td style="border-top:1px solid #e2e8e4; font-size:0; line-height:0;"> </td></tr></table>
|
||||||
${FOOTER.ar}
|
<div dir="ltr" lang="en" style="direction:ltr; text-align:left;">
|
||||||
</p>
|
${fillPlaceholders(mdToHtml(bodyEn), escaped(varsEn))}
|
||||||
<p style="font-size:12px; line-height:1.5; color:#666; ${BASE_FONT}">
|
${locationHtml(locationLine?.en)}
|
||||||
${FOOTER.en}
|
</div>
|
||||||
</p>
|
</td></tr></table>
|
||||||
${renderPartnerLogos()}`;
|
<p style="margin:32px 0 0; font-size:12px; line-height:1.5; color:#66716b; ${BASE_FONT}">
|
||||||
|
${FOOTER.ar}
|
||||||
|
</p>
|
||||||
|
<p style="margin:8px 0 0; font-size:12px; line-height:1.5; color:#66716b; ${BASE_FONT}">
|
||||||
|
${FOOTER.en}
|
||||||
|
</p>
|
||||||
|
${renderPartnerLogos()}
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
const text = [
|
const text = [
|
||||||
fillPlaceholders(mdToText(bodyAr), varsAr),
|
fillPlaceholders(mdToText(bodyAr), varsAr),
|
||||||
@@ -437,7 +449,7 @@ export function buildBilingualMail(opts: {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subject: `${fillPlaceholders(subjectAr, varsAr)} | ${fillPlaceholders(subjectEn, varsEn)}`,
|
subject,
|
||||||
text,
|
text,
|
||||||
html,
|
html,
|
||||||
};
|
};
|
||||||
@@ -489,7 +501,7 @@ type MailOptions = {
|
|||||||
to: string;
|
to: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
text: string;
|
text: string;
|
||||||
html?: string;
|
html: string;
|
||||||
attachments?: MailAttachment[];
|
attachments?: MailAttachment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -553,10 +565,8 @@ export async function sendRegistrationConfirmation({
|
|||||||
try {
|
try {
|
||||||
const isWaitlist = state === 'waitlisted';
|
const isWaitlist = state === 'waitlisted';
|
||||||
|
|
||||||
const [event, template] = await Promise.all([
|
const event = await fetchBilingualEvent(strapi, eventReference);
|
||||||
fetchBilingualEvent(strapi, eventReference),
|
const template = DEFAULT_EMAIL_TEMPLATES[isWaitlist ? 'waitlist' : 'confirmation'];
|
||||||
resolveEmailTemplate(strapi, isWaitlist ? 'waitlist' : 'confirmation'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!event.en && !event.ar) {
|
if (!event.en && !event.ar) {
|
||||||
strapi.log.warn(
|
strapi.log.warn(
|
||||||
@@ -623,10 +633,8 @@ export async function sendEventReminderMail({
|
|||||||
reservation: ReservationIdentity;
|
reservation: ReservationIdentity;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const [event, template] = await Promise.all([
|
const event = await fetchBilingualEvent(strapi, eventReference);
|
||||||
fetchBilingualEvent(strapi, eventReference),
|
const template = DEFAULT_EMAIL_TEMPLATES['event-reminder'];
|
||||||
resolveEmailTemplate(strapi, 'event-reminder'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!event.en && !event.ar) {
|
if (!event.en && !event.ar) {
|
||||||
strapi.log.warn(
|
strapi.log.warn(
|
||||||
|
|||||||
-28
@@ -110,33 +110,6 @@ export interface BlocksVideoEmbed extends Struct.ComponentSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailTemplate extends Struct.ComponentSchema {
|
|
||||||
collectionName: 'components_email_templates';
|
|
||||||
info: {
|
|
||||||
description: 'One outbound email template: a subject plus a markdown body. The body is converted to HTML at send time. Placeholders: {event}, {date} and {call}. A placeholder whose value is missing collapses together with the word before it, so "for {call}" (and "\u0639\u0644\u0649 {call}") reads cleanly when there is no call.';
|
|
||||||
displayName: 'Email Template';
|
|
||||||
icon: 'envelop';
|
|
||||||
};
|
|
||||||
attributes: {
|
|
||||||
body: Schema.Attribute.RichText & Schema.Attribute.Required;
|
|
||||||
enabled: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<true>;
|
|
||||||
key: Schema.Attribute.Enumeration<
|
|
||||||
[
|
|
||||||
'confirmation',
|
|
||||||
'waitlist',
|
|
||||||
'event-reminder',
|
|
||||||
'application-acknowledgement',
|
|
||||||
]
|
|
||||||
> &
|
|
||||||
Schema.Attribute.Required;
|
|
||||||
subject: Schema.Attribute.String &
|
|
||||||
Schema.Attribute.Required &
|
|
||||||
Schema.Attribute.SetMinMaxLength<{
|
|
||||||
maxLength: 250;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NavigationFooterColumn extends Struct.ComponentSchema {
|
export interface NavigationFooterColumn extends Struct.ComponentSchema {
|
||||||
collectionName: 'components_navigation_footer_columns';
|
collectionName: 'components_navigation_footer_columns';
|
||||||
info: {
|
info: {
|
||||||
@@ -393,7 +366,6 @@ declare module '@strapi/strapi' {
|
|||||||
'blocks.quote': BlocksQuote;
|
'blocks.quote': BlocksQuote;
|
||||||
'blocks.rich-text': BlocksRichText;
|
'blocks.rich-text': BlocksRichText;
|
||||||
'blocks.video-embed': BlocksVideoEmbed;
|
'blocks.video-embed': BlocksVideoEmbed;
|
||||||
'email.template': EmailTemplate;
|
|
||||||
'navigation.footer-column': NavigationFooterColumn;
|
'navigation.footer-column': NavigationFooterColumn;
|
||||||
'navigation.nav-group': NavigationNavGroup;
|
'navigation.nav-group': NavigationNavGroup;
|
||||||
'page.band-intro': PageBandIntro;
|
'page.band-intro': PageBandIntro;
|
||||||
|
|||||||
-40
@@ -852,45 +852,6 @@ export interface ApiContactSubmissionContactSubmission
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiEmailSettingsEmailSettings extends Struct.SingleTypeSchema {
|
|
||||||
collectionName: 'email_settings';
|
|
||||||
info: {
|
|
||||||
description: 'The outbound email templates (registration confirmation, waitlist, reminder, application acknowledgement). Markdown bodies become HTML at send time. The values seeded here on first boot are also the defaults the mails fall back to when the row is missing. Read by the mail jobs only; not exposed over the public API.';
|
|
||||||
displayName: 'Email Settings';
|
|
||||||
pluralName: 'email-settings-list';
|
|
||||||
singularName: 'email-settings';
|
|
||||||
};
|
|
||||||
options: {
|
|
||||||
draftAndPublish: false;
|
|
||||||
};
|
|
||||||
pluginOptions: {
|
|
||||||
i18n: {
|
|
||||||
localized: true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
attributes: {
|
|
||||||
createdAt: Schema.Attribute.DateTime;
|
|
||||||
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
|
||||||
Schema.Attribute.Private;
|
|
||||||
locale: Schema.Attribute.String;
|
|
||||||
localizations: Schema.Attribute.Relation<
|
|
||||||
'oneToMany',
|
|
||||||
'api::email-settings.email-settings'
|
|
||||||
>;
|
|
||||||
publishedAt: Schema.Attribute.DateTime;
|
|
||||||
templates: Schema.Attribute.Component<'email.template', true> &
|
|
||||||
Schema.Attribute.Required &
|
|
||||||
Schema.Attribute.SetPluginOptions<{
|
|
||||||
i18n: {
|
|
||||||
localized: true;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
updatedAt: Schema.Attribute.DateTime;
|
|
||||||
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
|
||||||
Schema.Attribute.Private;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiEventRegistrationEventRegistration
|
export interface ApiEventRegistrationEventRegistration
|
||||||
extends Struct.CollectionTypeSchema {
|
extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'event_registrations';
|
collectionName: 'event_registrations';
|
||||||
@@ -2869,7 +2830,6 @@ declare module '@strapi/strapi' {
|
|||||||
'api::application.application': ApiApplicationApplication;
|
'api::application.application': ApiApplicationApplication;
|
||||||
'api::contact-page.contact-page': ApiContactPageContactPage;
|
'api::contact-page.contact-page': ApiContactPageContactPage;
|
||||||
'api::contact-submission.contact-submission': ApiContactSubmissionContactSubmission;
|
'api::contact-submission.contact-submission': ApiContactSubmissionContactSubmission;
|
||||||
'api::email-settings.email-settings': ApiEmailSettingsEmailSettings;
|
|
||||||
'api::event-registration.event-registration': ApiEventRegistrationEventRegistration;
|
'api::event-registration.event-registration': ApiEventRegistrationEventRegistration;
|
||||||
'api::event.event': ApiEventEvent;
|
'api::event.event': ApiEventEvent;
|
||||||
'api::faq.faq': ApiFaqFaq;
|
'api::faq.faq': ApiFaqFaq;
|
||||||
|
|||||||
Reference in New Issue
Block a user