@consilioweb/payload-support 0.5.1
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/LICENSE +21 -0
- package/README.md +525 -0
- package/dist/client.cjs +7 -0
- package/dist/client.d.cts +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.cjs +7766 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +7730 -0
- package/dist/views.d.cts +30 -0
- package/dist/views.d.ts +30 -0
- package/package.json +131 -0
- package/src/client.ts +1 -0
- package/src/collections/AuthLogs.ts +65 -0
- package/src/collections/CannedResponses.ts +69 -0
- package/src/collections/ChatMessages.ts +98 -0
- package/src/collections/EmailLogs.ts +94 -0
- package/src/collections/KnowledgeBase.ts +99 -0
- package/src/collections/Macros.ts +98 -0
- package/src/collections/PendingEmails.ts +122 -0
- package/src/collections/SatisfactionSurveys.ts +98 -0
- package/src/collections/SlaPolicies.ts +123 -0
- package/src/collections/SupportClients.ts +210 -0
- package/src/collections/TicketActivityLog.ts +81 -0
- package/src/collections/TicketMessages.ts +364 -0
- package/src/collections/TicketStatuses.ts +108 -0
- package/src/collections/Tickets.ts +704 -0
- package/src/collections/TimeEntries.ts +105 -0
- package/src/collections/WebhookEndpoints.ts +96 -0
- package/src/collections/index.ts +16 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
- package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
- package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
- package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
- package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
- package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
- package/src/components/TicketConversation/config.ts +82 -0
- package/src/components/TicketConversation/constants.ts +74 -0
- package/src/components/TicketConversation/context.ts +63 -0
- package/src/components/TicketConversation/hooks/useAI.ts +180 -0
- package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
- package/src/components/TicketConversation/hooks/useReply.ts +190 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
- package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
- package/src/components/TicketConversation/index.tsx +1110 -0
- package/src/components/TicketConversation/locales/en.json +878 -0
- package/src/components/TicketConversation/locales/fr.json +878 -0
- package/src/components/TicketConversation/types.ts +54 -0
- package/src/components/TicketConversation/utils.ts +25 -0
- package/src/endpoints/admin-chat-stream.ts +238 -0
- package/src/endpoints/admin-chat.ts +263 -0
- package/src/endpoints/admin-stats.ts +200 -0
- package/src/endpoints/ai.ts +199 -0
- package/src/endpoints/apply-macro.ts +144 -0
- package/src/endpoints/auth-2fa.ts +163 -0
- package/src/endpoints/auto-close.ts +175 -0
- package/src/endpoints/billing.ts +167 -0
- package/src/endpoints/bulk-action.ts +103 -0
- package/src/endpoints/chat-stream.ts +127 -0
- package/src/endpoints/chat.ts +188 -0
- package/src/endpoints/chatbot.ts +113 -0
- package/src/endpoints/delete-account.ts +129 -0
- package/src/endpoints/email-stats.ts +109 -0
- package/src/endpoints/export-csv.ts +84 -0
- package/src/endpoints/export-data.ts +104 -0
- package/src/endpoints/import-conversation.ts +307 -0
- package/src/endpoints/index.ts +154 -0
- package/src/endpoints/login.ts +92 -0
- package/src/endpoints/merge-clients.ts +132 -0
- package/src/endpoints/merge-tickets.ts +137 -0
- package/src/endpoints/oauth-google.ts +179 -0
- package/src/endpoints/pending-emails-process.ts +224 -0
- package/src/endpoints/presence.ts +104 -0
- package/src/endpoints/process-scheduled.ts +144 -0
- package/src/endpoints/purge-logs.ts +58 -0
- package/src/endpoints/resend-notification.ts +99 -0
- package/src/endpoints/round-robin-config.ts +92 -0
- package/src/endpoints/satisfaction.ts +93 -0
- package/src/endpoints/search.ts +106 -0
- package/src/endpoints/seed-kb.ts +153 -0
- package/src/endpoints/settings.ts +144 -0
- package/src/endpoints/signature.ts +93 -0
- package/src/endpoints/sla-check.ts +124 -0
- package/src/endpoints/split-ticket.ts +131 -0
- package/src/endpoints/statuses.ts +45 -0
- package/src/endpoints/track-open.ts +154 -0
- package/src/endpoints/typing.ts +101 -0
- package/src/endpoints/user-prefs.ts +125 -0
- package/src/hooks/checkSLA.ts +414 -0
- package/src/hooks/ticketStatusEmail.ts +182 -0
- package/src/index.ts +51 -0
- package/src/plugin.ts +157 -0
- package/src/portal/LiveChat.tsx +1353 -0
- package/src/portal/auth/ChatWidget.tsx +350 -0
- package/src/portal/auth/ChatbotWidget.tsx +285 -0
- package/src/portal/auth/SupportHeader.tsx +409 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
- package/src/portal/auth/dashboard/page.tsx +84 -0
- package/src/portal/auth/faq/FAQSearch.tsx +117 -0
- package/src/portal/auth/faq/page.tsx +199 -0
- package/src/portal/auth/layout.tsx +61 -0
- package/src/portal/auth/profile/page.tsx +705 -0
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
- package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
- package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
- package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
- package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
- package/src/portal/auth/tickets/detail/page.tsx +738 -0
- package/src/portal/auth/tickets/new/page.tsx +515 -0
- package/src/portal/forgot-password/page.tsx +114 -0
- package/src/portal/layout.tsx +26 -0
- package/src/portal/locales/en.json +374 -0
- package/src/portal/locales/fr.json +374 -0
- package/src/portal/login/page.tsx +351 -0
- package/src/portal/page.tsx +162 -0
- package/src/portal/register/page.tsx +281 -0
- package/src/portal/reset-password/page.tsx +152 -0
- package/src/styles/BillingView.module.scss +311 -0
- package/src/styles/ChatView.module.scss +438 -0
- package/src/styles/CommandPalette.module.scss +160 -0
- package/src/styles/CrmView.module.scss +554 -0
- package/src/styles/EmailTracking.module.scss +238 -0
- package/src/styles/ImportConversation.module.scss +267 -0
- package/src/styles/Layout.module.scss +55 -0
- package/src/styles/Logs.module.scss +164 -0
- package/src/styles/NewTicket.module.scss +143 -0
- package/src/styles/PendingEmails.module.scss +629 -0
- package/src/styles/SupportDashboard.module.scss +649 -0
- package/src/styles/TicketDetail.module.scss +1043 -0
- package/src/styles/TicketInbox.module.scss +296 -0
- package/src/styles/TicketingSettings.module.scss +358 -0
- package/src/styles/TimeDashboard.module.scss +287 -0
- package/src/styles/_tokens.scss +78 -0
- package/src/styles/theme.css +633 -0
- package/src/types.ts +255 -0
- package/src/utils/adminNotification.ts +38 -0
- package/src/utils/auth.ts +46 -0
- package/src/utils/emailTemplate.ts +343 -0
- package/src/utils/fireWebhooks.ts +84 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/rateLimiter.ts +52 -0
- package/src/utils/readSettings.ts +67 -0
- package/src/utils/slugs.ts +54 -0
- package/src/utils/webhookDispatcher.ts +120 -0
- package/src/views/BillingView/client.tsx +137 -0
- package/src/views/BillingView/index.tsx +33 -0
- package/src/views/ChatView/client.tsx +294 -0
- package/src/views/ChatView/index.tsx +33 -0
- package/src/views/CrmView/client.tsx +206 -0
- package/src/views/CrmView/index.tsx +33 -0
- package/src/views/EmailTrackingView/client.tsx +124 -0
- package/src/views/EmailTrackingView/index.tsx +33 -0
- package/src/views/ImportConversationView/client.tsx +133 -0
- package/src/views/ImportConversationView/index.tsx +33 -0
- package/src/views/LogsView/client.tsx +151 -0
- package/src/views/LogsView/index.tsx +30 -0
- package/src/views/NewTicketView/client.tsx +227 -0
- package/src/views/NewTicketView/index.tsx +30 -0
- package/src/views/PendingEmailsView/client.tsx +177 -0
- package/src/views/PendingEmailsView/index.tsx +33 -0
- package/src/views/SupportDashboardView/client.tsx +424 -0
- package/src/views/SupportDashboardView/index.tsx +33 -0
- package/src/views/TicketDetailView/client.tsx +775 -0
- package/src/views/TicketDetailView/index.tsx +33 -0
- package/src/views/TicketInboxView/client.tsx +313 -0
- package/src/views/TicketInboxView/index.tsx +30 -0
- package/src/views/TicketingSettingsView/client.tsx +866 -0
- package/src/views/TicketingSettingsView/index.tsx +33 -0
- package/src/views/TimeDashboardView/client.tsx +144 -0
- package/src/views/TimeDashboardView/index.tsx +33 -0
- package/src/views/shared/AdminViewHeader.tsx +69 -0
- package/src/views/shared/ErrorBoundary.tsx +68 -0
- package/src/views/shared/Skeleton.tsx +125 -0
- package/src/views/shared/adminTokens.ts +37 -0
- package/src/views/shared/config.ts +82 -0
- package/src/views/shared/index.ts +6 -0
- package/src/views.ts +16 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// ─── Feature flags ───────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export interface SupportFeatures {
|
|
4
|
+
/** Time tracking: timer, manual entries, billing */
|
|
5
|
+
timeTracking?: boolean
|
|
6
|
+
/** AI features: sentiment, synthesis, suggestion, rewrite */
|
|
7
|
+
ai?: boolean
|
|
8
|
+
/** Satisfaction surveys: CSAT rating after resolution */
|
|
9
|
+
satisfaction?: boolean
|
|
10
|
+
/** Live chat integration: chat → ticket conversion */
|
|
11
|
+
chat?: boolean
|
|
12
|
+
/** Email tracking: pixel tracking, open/sent status per message */
|
|
13
|
+
emailTracking?: boolean
|
|
14
|
+
/** Canned responses: quick reply templates */
|
|
15
|
+
canned?: boolean
|
|
16
|
+
/** Ticket merge: combine two tickets into one */
|
|
17
|
+
merge?: boolean
|
|
18
|
+
/** Snooze: temporarily hide a ticket */
|
|
19
|
+
snooze?: boolean
|
|
20
|
+
/** External messages: add messages received outside the system */
|
|
21
|
+
externalMessages?: boolean
|
|
22
|
+
/** Client history: past tickets, projects, notes sidebar */
|
|
23
|
+
clientHistory?: boolean
|
|
24
|
+
/** Activity log: audit trail of actions on the ticket */
|
|
25
|
+
activityLog?: boolean
|
|
26
|
+
/** Split ticket: extract a message into a new ticket */
|
|
27
|
+
splitTicket?: boolean
|
|
28
|
+
/** Scheduled replies: send a message at a future date */
|
|
29
|
+
scheduledReplies?: boolean
|
|
30
|
+
/** Auto-close: automatically resolve inactive tickets */
|
|
31
|
+
autoClose?: boolean
|
|
32
|
+
/** Auto-close delay in days */
|
|
33
|
+
autoCloseDays?: number
|
|
34
|
+
/** Round-robin: distribute new tickets evenly among agents */
|
|
35
|
+
roundRobin?: boolean
|
|
36
|
+
/** SLA policies: response & resolution time targets */
|
|
37
|
+
sla?: boolean
|
|
38
|
+
/** Webhooks: outbound HTTP hooks on ticket events */
|
|
39
|
+
webhooks?: boolean
|
|
40
|
+
/** Macros: multi-action shortcuts */
|
|
41
|
+
macros?: boolean
|
|
42
|
+
/** Custom statuses: configurable ticket statuses */
|
|
43
|
+
customStatuses?: boolean
|
|
44
|
+
/** Collision detection: warn when multiple agents view same ticket */
|
|
45
|
+
collisionDetection?: boolean
|
|
46
|
+
/** Per-agent email signatures */
|
|
47
|
+
signatures?: boolean
|
|
48
|
+
/** AI chatbot for self-service */
|
|
49
|
+
chatbot?: boolean
|
|
50
|
+
/** Bulk actions on multiple tickets */
|
|
51
|
+
bulkActions?: boolean
|
|
52
|
+
/** Command palette (⌘K) */
|
|
53
|
+
commandPalette?: boolean
|
|
54
|
+
/** Knowledge base / FAQ */
|
|
55
|
+
knowledgeBase?: boolean
|
|
56
|
+
/** Pending email queue */
|
|
57
|
+
pendingEmails?: boolean
|
|
58
|
+
/** Authentication audit logs */
|
|
59
|
+
authLogs?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── AI provider ─────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface AIProviderConfig {
|
|
65
|
+
provider: 'anthropic' | 'openai' | 'ollama' | 'custom'
|
|
66
|
+
apiKey?: string
|
|
67
|
+
model?: string
|
|
68
|
+
baseUrl?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Email configuration ─────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export interface EmailConfig {
|
|
74
|
+
fromAddress?: string
|
|
75
|
+
fromName?: string
|
|
76
|
+
replyTo?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Plugin configuration ────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export interface SupportPluginConfig {
|
|
82
|
+
/** Enable/disable individual features (all enabled by default) */
|
|
83
|
+
features?: SupportFeatures
|
|
84
|
+
|
|
85
|
+
/** AI provider configuration */
|
|
86
|
+
ai?: AIProviderConfig
|
|
87
|
+
|
|
88
|
+
/** Email configuration for ticket notifications */
|
|
89
|
+
email?: EmailConfig
|
|
90
|
+
|
|
91
|
+
/** Locale: 'fr' or 'en' (default: 'fr') */
|
|
92
|
+
locale?: 'fr' | 'en'
|
|
93
|
+
|
|
94
|
+
/** Nav group label in Payload admin sidebar */
|
|
95
|
+
navGroup?: string
|
|
96
|
+
|
|
97
|
+
/** Base path for admin views (default: '/support') */
|
|
98
|
+
basePath?: string
|
|
99
|
+
|
|
100
|
+
/** User collection slug for agent relationships (default: 'users') */
|
|
101
|
+
userCollectionSlug?: string
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Restrict Google OAuth auto-registration to specific email domains.
|
|
105
|
+
* When set and non-empty, only emails matching one of these domains can
|
|
106
|
+
* create an account via OAuth. Existing accounts are unaffected.
|
|
107
|
+
* Example: ['acme.com', 'partner.org']
|
|
108
|
+
*/
|
|
109
|
+
allowedEmailDomains?: string[]
|
|
110
|
+
|
|
111
|
+
/** Skip injecting collections (use your own custom collections) */
|
|
112
|
+
skipCollections?: boolean
|
|
113
|
+
|
|
114
|
+
/** Skip injecting admin views (use your own custom views) */
|
|
115
|
+
skipViews?: boolean
|
|
116
|
+
|
|
117
|
+
/** Skip injecting endpoints (use your own custom API routes) */
|
|
118
|
+
skipEndpoints?: boolean
|
|
119
|
+
|
|
120
|
+
/** Collection slug overrides */
|
|
121
|
+
collectionSlugs?: {
|
|
122
|
+
tickets?: string
|
|
123
|
+
ticketMessages?: string
|
|
124
|
+
supportClients?: string
|
|
125
|
+
timeEntries?: string
|
|
126
|
+
cannedResponses?: string
|
|
127
|
+
ticketActivityLog?: string
|
|
128
|
+
satisfactionSurveys?: string
|
|
129
|
+
knowledgeBase?: string
|
|
130
|
+
chatMessages?: string
|
|
131
|
+
pendingEmails?: string
|
|
132
|
+
emailLogs?: string
|
|
133
|
+
authLogs?: string
|
|
134
|
+
webhookEndpoints?: string
|
|
135
|
+
slaPolicies?: string
|
|
136
|
+
macros?: string
|
|
137
|
+
ticketStatuses?: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Admin notification collection slug (default: 'admin-notifications') */
|
|
141
|
+
notificationSlug?: string
|
|
142
|
+
|
|
143
|
+
/** Custom component path for ticket conversation UI field */
|
|
144
|
+
conversationComponent?: string
|
|
145
|
+
|
|
146
|
+
/** Project collection slug — adds a project relationship to tickets (optional) */
|
|
147
|
+
projectCollectionSlug?: string
|
|
148
|
+
|
|
149
|
+
/** Documents upload collection slug — adds quote/invoice upload fields to tickets (optional) */
|
|
150
|
+
documentsCollectionSlug?: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Ticket data types ───────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export interface TicketData {
|
|
156
|
+
id: number | string
|
|
157
|
+
ticketNumber: string
|
|
158
|
+
subject: string
|
|
159
|
+
status: string
|
|
160
|
+
priority: string
|
|
161
|
+
category?: string
|
|
162
|
+
client?: number | string
|
|
163
|
+
assignedTo?: number | string
|
|
164
|
+
totalTimeMinutes?: number
|
|
165
|
+
createdAt: string
|
|
166
|
+
updatedAt: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface MessageData {
|
|
170
|
+
id: number | string
|
|
171
|
+
ticket: number | string
|
|
172
|
+
body: string
|
|
173
|
+
bodyHtml?: string
|
|
174
|
+
authorType: 'admin' | 'client' | 'email'
|
|
175
|
+
isInternal?: boolean
|
|
176
|
+
attachments?: Array<{ file: number | string }>
|
|
177
|
+
createdAt: string
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface TimeEntryData {
|
|
181
|
+
id: number | string
|
|
182
|
+
ticket: number | string
|
|
183
|
+
minutes: number
|
|
184
|
+
description?: string
|
|
185
|
+
date: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ClientData {
|
|
189
|
+
id: number | string
|
|
190
|
+
email: string
|
|
191
|
+
firstName: string
|
|
192
|
+
lastName: string
|
|
193
|
+
company?: string
|
|
194
|
+
phone?: string
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface CannedResponseData {
|
|
198
|
+
id: number | string
|
|
199
|
+
title: string
|
|
200
|
+
body: string
|
|
201
|
+
category?: string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface ActivityEntryData {
|
|
205
|
+
id: number | string
|
|
206
|
+
ticket: number | string
|
|
207
|
+
action: string
|
|
208
|
+
field?: string
|
|
209
|
+
oldValue?: string
|
|
210
|
+
newValue?: string
|
|
211
|
+
actorType: 'admin' | 'client' | 'system'
|
|
212
|
+
actorEmail?: string
|
|
213
|
+
createdAt: string
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface SatisfactionSurveyData {
|
|
217
|
+
id: number | string
|
|
218
|
+
ticket: number | string
|
|
219
|
+
client: number | string
|
|
220
|
+
rating: number
|
|
221
|
+
comment?: string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Default feature values ──────────────────────────────
|
|
225
|
+
|
|
226
|
+
export const DEFAULT_FEATURES: Required<SupportFeatures> = {
|
|
227
|
+
timeTracking: true,
|
|
228
|
+
ai: true,
|
|
229
|
+
satisfaction: true,
|
|
230
|
+
chat: true,
|
|
231
|
+
emailTracking: true,
|
|
232
|
+
canned: true,
|
|
233
|
+
merge: true,
|
|
234
|
+
snooze: true,
|
|
235
|
+
externalMessages: true,
|
|
236
|
+
clientHistory: true,
|
|
237
|
+
activityLog: true,
|
|
238
|
+
splitTicket: true,
|
|
239
|
+
scheduledReplies: true,
|
|
240
|
+
autoClose: true,
|
|
241
|
+
autoCloseDays: 7,
|
|
242
|
+
roundRobin: false,
|
|
243
|
+
sla: true,
|
|
244
|
+
webhooks: true,
|
|
245
|
+
macros: true,
|
|
246
|
+
customStatuses: false,
|
|
247
|
+
collisionDetection: true,
|
|
248
|
+
signatures: true,
|
|
249
|
+
chatbot: true,
|
|
250
|
+
bulkActions: true,
|
|
251
|
+
commandPalette: true,
|
|
252
|
+
knowledgeBase: true,
|
|
253
|
+
pendingEmails: true,
|
|
254
|
+
authLogs: true,
|
|
255
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Payload } from 'payload'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper to create an admin notification.
|
|
5
|
+
* Can be called from any hook or endpoint.
|
|
6
|
+
*
|
|
7
|
+
* @param payload - Payload instance
|
|
8
|
+
* @param data - Notification data
|
|
9
|
+
* @param collectionSlug - Override collection slug (default: 'admin-notifications')
|
|
10
|
+
*/
|
|
11
|
+
export async function createAdminNotification(
|
|
12
|
+
payload: Payload,
|
|
13
|
+
data: {
|
|
14
|
+
title: string
|
|
15
|
+
message?: string
|
|
16
|
+
type: 'info' | 'new_ticket' | 'client_message' | 'quote_request' | 'urgent_ticket' | 'post_published' | 'satisfaction' | 'sla_alert'
|
|
17
|
+
link?: string
|
|
18
|
+
recipient?: number | string
|
|
19
|
+
},
|
|
20
|
+
collectionSlug = 'admin-notifications',
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
await payload.create({
|
|
24
|
+
collection: collectionSlug as any,
|
|
25
|
+
data: {
|
|
26
|
+
title: data.title,
|
|
27
|
+
message: data.message,
|
|
28
|
+
type: data.type,
|
|
29
|
+
link: data.link,
|
|
30
|
+
recipient: data.recipient,
|
|
31
|
+
read: false,
|
|
32
|
+
},
|
|
33
|
+
overrideAccess: true,
|
|
34
|
+
})
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('[notification] Failed to create:', err)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CollectionSlugs } from './slugs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom error class for authentication/authorization failures.
|
|
5
|
+
* Carries an HTTP status code for consistent API responses.
|
|
6
|
+
*/
|
|
7
|
+
export class AuthError extends Error {
|
|
8
|
+
public readonly statusCode: number
|
|
9
|
+
|
|
10
|
+
constructor(message: string, statusCode: number) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'AuthError'
|
|
13
|
+
this.statusCode = statusCode
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Asserts the request comes from an authenticated admin user.
|
|
19
|
+
* Throws AuthError(401) if unauthenticated, AuthError(403) if not an admin.
|
|
20
|
+
* Uses TypeScript assertion signature so `req.user` is narrowed to non-null after call.
|
|
21
|
+
*/
|
|
22
|
+
export function requireAdmin(req: { user?: any }, slugs: CollectionSlugs): asserts req is { user: NonNullable<typeof req.user> } {
|
|
23
|
+
if (!req.user) throw new AuthError('Authentication required', 401)
|
|
24
|
+
if (req.user.collection !== slugs.users) throw new AuthError('Admin access required', 403)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Asserts the request comes from an authenticated support client.
|
|
29
|
+
* Throws AuthError(401) if unauthenticated, AuthError(403) if not a client.
|
|
30
|
+
* Uses TypeScript assertion signature so `req.user` is narrowed to non-null after call.
|
|
31
|
+
*/
|
|
32
|
+
export function requireClient(req: { user?: any }, slugs: CollectionSlugs): asserts req is { user: NonNullable<typeof req.user> } {
|
|
33
|
+
if (!req.user) throw new AuthError('Authentication required', 401)
|
|
34
|
+
if (req.user.collection !== slugs.supportClients) throw new AuthError('Client access required', 403)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handles AuthError in catch blocks, returning appropriate JSON responses.
|
|
39
|
+
* Returns null if the error is not an AuthError (caller should handle it).
|
|
40
|
+
*/
|
|
41
|
+
export function handleAuthError(error: unknown): Response | null {
|
|
42
|
+
if (error instanceof AuthError) {
|
|
43
|
+
return Response.json({ error: error.message }, { status: error.statusCode })
|
|
44
|
+
}
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Professional B2B email template system
|
|
3
|
+
* Configurable branding, colors, and layout
|
|
4
|
+
* Tone: Professional, responsive email design
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── Configuration ───────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface EmailTemplateConfig {
|
|
10
|
+
brandName?: string
|
|
11
|
+
brandColor?: string // primary accent color (hex)
|
|
12
|
+
secondaryColor?: string // secondary accent color (hex)
|
|
13
|
+
accentColor?: string // tertiary accent color (hex)
|
|
14
|
+
logoUrl?: string
|
|
15
|
+
supportEmail?: string
|
|
16
|
+
websiteUrl?: string
|
|
17
|
+
phone?: string
|
|
18
|
+
location?: string
|
|
19
|
+
/** Short brand initials shown in header badge (e.g. "CW", "AB") */
|
|
20
|
+
brandInitials?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CONFIG: Required<EmailTemplateConfig> = {
|
|
24
|
+
brandName: 'Support',
|
|
25
|
+
brandColor: '#00E5FF',
|
|
26
|
+
secondaryColor: '#FFD600',
|
|
27
|
+
accentColor: '#FF8A00',
|
|
28
|
+
logoUrl: '',
|
|
29
|
+
supportEmail: process.env.SUPPORT_EMAIL || '',
|
|
30
|
+
websiteUrl: process.env.NEXT_PUBLIC_SERVER_URL || '',
|
|
31
|
+
phone: '',
|
|
32
|
+
location: '',
|
|
33
|
+
brandInitials: '',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveConfig(config?: EmailTemplateConfig): Required<EmailTemplateConfig> {
|
|
37
|
+
return { ...DEFAULT_CONFIG, ...config }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Brand-agnostic utilities ────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export function escapeHtml(str: string): string {
|
|
43
|
+
return str
|
|
44
|
+
.replace(/&/g, '&')
|
|
45
|
+
.replace(/</g, '<')
|
|
46
|
+
.replace(/>/g, '>')
|
|
47
|
+
.replace(/"/g, '"')
|
|
48
|
+
.replace(/'/g, ''')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tracking pixel for email open detection
|
|
53
|
+
* Inserts a 1x1 transparent GIF that triggers /api/support/track-open
|
|
54
|
+
*/
|
|
55
|
+
export function emailTrackingPixel(ticketId: number | string, messageId?: number | string, baseUrl?: string): string {
|
|
56
|
+
const url = baseUrl || process.env.NEXT_PUBLIC_SERVER_URL || ''
|
|
57
|
+
const params = `t=${ticketId}${messageId ? `&m=${messageId}` : ''}`
|
|
58
|
+
return `<img src="${url}/api/support/track-open?${params}" width="1" height="1" alt="" style="display:block;width:1px;height:1px;border:0;" />`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert WYSIWYG HTML to email-safe HTML with inline styles.
|
|
63
|
+
* Handles blockquotes, images, links, lists, and paragraphs.
|
|
64
|
+
*/
|
|
65
|
+
export function emailRichContent(html: string, config?: EmailTemplateConfig): string {
|
|
66
|
+
const c = resolveConfig(config)
|
|
67
|
+
const baseUrl = c.websiteUrl
|
|
68
|
+
|
|
69
|
+
// Helper: replace tag with inline-styled version (strips existing style)
|
|
70
|
+
function styleTag(input: string, tag: string, style: string): string {
|
|
71
|
+
const regex = new RegExp(`<${tag}(\\s[^>]*)?>`, 'gi')
|
|
72
|
+
return input.replace(regex, (_match, attrs) => {
|
|
73
|
+
const cleanAttrs = (attrs || '').replace(/\s*style="[^"]*"/g, '')
|
|
74
|
+
return `<${tag}${cleanAttrs} style="${style}">`
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let result = html
|
|
79
|
+
// Make relative image URLs absolute
|
|
80
|
+
.replace(/src="\/([^"]+)"/g, `src="${baseUrl}/$1"`)
|
|
81
|
+
|
|
82
|
+
// Apply inline styles to elements
|
|
83
|
+
result = styleTag(result, 'blockquote', `border-left: 4px solid ${c.brandColor}; margin: 16px 0; padding: 12px 20px; background: #f0f9fa; border-radius: 0 8px 8px 0;`)
|
|
84
|
+
result = styleTag(result, 'img', 'max-width: 100%; height: auto; border-radius: 8px; margin: 12px 0; display: block;')
|
|
85
|
+
result = styleTag(result, 'a', `color: ${c.brandColor}; text-decoration: underline; font-weight: 600;`)
|
|
86
|
+
result = styleTag(result, 'ul', 'margin: 8px 0; padding-left: 24px;')
|
|
87
|
+
result = styleTag(result, 'ol', 'margin: 8px 0; padding-left: 24px;')
|
|
88
|
+
result = styleTag(result, 'li', 'margin: 4px 0; line-height: 1.6;')
|
|
89
|
+
result = styleTag(result, 'p', 'margin: 0 0 12px 0; line-height: 1.75; font-size: 15px; color: #1f2937;')
|
|
90
|
+
|
|
91
|
+
// Strip dangerous content
|
|
92
|
+
result = result
|
|
93
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
94
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
|
95
|
+
.replace(/\bon\w+\s*=\s*"[^"]*"/gi, '')
|
|
96
|
+
.replace(/\bon\w+\s*=\s*'[^']*'/gi, '')
|
|
97
|
+
.replace(/javascript:/gi, '')
|
|
98
|
+
|
|
99
|
+
return `<div style="font-size: 15px; line-height: 1.75; color: #1f2937;">${result}</div>`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Configurable email components ───────────────────────────────────
|
|
103
|
+
|
|
104
|
+
type ButtonColor = 'primary' | 'secondary' | 'dark'
|
|
105
|
+
|
|
106
|
+
function getButtonColors(config: Required<EmailTemplateConfig>): Record<ButtonColor, { bg: string; text: string; border: string }> {
|
|
107
|
+
return {
|
|
108
|
+
primary: { bg: config.brandColor, text: '#000000', border: '#000000' },
|
|
109
|
+
secondary: { bg: config.secondaryColor, text: '#000000', border: '#000000' },
|
|
110
|
+
dark: { bg: '#000000', text: '#FFFFFF', border: '#000000' },
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate a CTA button for emails
|
|
116
|
+
*/
|
|
117
|
+
export function emailButton(text: string, url: string, color: ButtonColor = 'primary', config?: EmailTemplateConfig): string {
|
|
118
|
+
const c = resolveConfig(config)
|
|
119
|
+
const colors = getButtonColors(c)
|
|
120
|
+
const bc = colors[color]
|
|
121
|
+
return `
|
|
122
|
+
<div style="text-align: center; margin: 32px 0;">
|
|
123
|
+
<a href="${url}" style="display: inline-block; padding: 16px 40px; background: ${bc.bg}; color: ${bc.text}; font-weight: 800; font-size: 15px; text-decoration: none; border-radius: 10px; border: 2px solid ${bc.border}; letter-spacing: 0.02em;">
|
|
124
|
+
${text}
|
|
125
|
+
</a>
|
|
126
|
+
</div>
|
|
127
|
+
`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Quote block for message previews
|
|
132
|
+
*/
|
|
133
|
+
export function emailQuote(content: string, borderColor?: string, config?: EmailTemplateConfig): string {
|
|
134
|
+
const c = resolveConfig(config)
|
|
135
|
+
const color = borderColor || c.brandColor
|
|
136
|
+
return `
|
|
137
|
+
<div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
|
|
138
|
+
<p style="margin: 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(content)}</p>
|
|
139
|
+
</div>
|
|
140
|
+
`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Info row (label: value) for structured data in emails
|
|
145
|
+
*/
|
|
146
|
+
export function emailInfoRow(label: string, value: string): string {
|
|
147
|
+
return `
|
|
148
|
+
<tr>
|
|
149
|
+
<td style="padding: 8px 0; font-size: 14px; font-weight: 700; color: #333333; width: 150px; vertical-align: top; text-transform: uppercase; letter-spacing: 0.03em; font-size: 12px;">${escapeHtml(label)}</td>
|
|
150
|
+
<td style="padding: 8px 0; font-size: 15px; color: #1f2937; line-height: 1.5;">${value}</td>
|
|
151
|
+
</tr>
|
|
152
|
+
`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Paragraph helper with professional styling
|
|
157
|
+
*/
|
|
158
|
+
export function emailParagraph(text: string): string {
|
|
159
|
+
return `<p style="margin: 0 0 18px 0; font-size: 15px; line-height: 1.75; color: #1f2937;">${text}</p>`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generate the professional footer
|
|
164
|
+
*/
|
|
165
|
+
function emailFooter(config: Required<EmailTemplateConfig>): string {
|
|
166
|
+
const logoHtml = config.logoUrl
|
|
167
|
+
? `<a href="${config.websiteUrl}">
|
|
168
|
+
<img src="${config.logoUrl}" alt="${escapeHtml(config.brandName)}" width="100" height="47" style="display: block; border: 0;" />
|
|
169
|
+
</a>`
|
|
170
|
+
: `<a href="${config.websiteUrl}" style="font-size: 18px; font-weight: 900; color: #000000; text-decoration: none;">${escapeHtml(config.brandName)}</a>`
|
|
171
|
+
|
|
172
|
+
const contactParts: string[] = []
|
|
173
|
+
if (config.supportEmail) {
|
|
174
|
+
contactParts.push(`<a href="mailto:${config.supportEmail}" style="color: #555555; text-decoration: none;">${config.supportEmail}</a>`)
|
|
175
|
+
}
|
|
176
|
+
if (config.phone) {
|
|
177
|
+
contactParts.push(`<a href="tel:${config.phone.replace(/\s/g, '')}" style="color: #555555; text-decoration: none;">${config.phone}</a>`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const locationParts: string[] = []
|
|
181
|
+
if (config.websiteUrl) {
|
|
182
|
+
const displayUrl = config.websiteUrl.replace(/^https?:\/\//, '')
|
|
183
|
+
locationParts.push(`<a href="${config.websiteUrl}" style="color: ${config.brandColor}; text-decoration: none; font-weight: 600;">${displayUrl}</a>`)
|
|
184
|
+
}
|
|
185
|
+
if (config.location) {
|
|
186
|
+
locationParts.push(config.location)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const unsubscribeHtml = config.supportEmail
|
|
190
|
+
? `<p style="margin: 8px 0 0 0; font-size: 11px; color: #aaaaaa; line-height: 1.4;">
|
|
191
|
+
<a href="mailto:${config.supportEmail}?subject=Unsubscribe" style="color: #aaaaaa; text-decoration: underline;">Se désinscrire</a>
|
|
192
|
+
</p>`
|
|
193
|
+
: ''
|
|
194
|
+
|
|
195
|
+
return `
|
|
196
|
+
<!-- Spacer -->
|
|
197
|
+
<div style="height: 24px;"></div>
|
|
198
|
+
|
|
199
|
+
<!-- Tricolor separator -->
|
|
200
|
+
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="margin-bottom: 24px;">
|
|
201
|
+
<tr>
|
|
202
|
+
<td width="50%" height="3" bgcolor="${config.brandColor}" style="font-size:1px;line-height:1px;"> </td>
|
|
203
|
+
<td width="25%" height="3" bgcolor="${config.secondaryColor}" style="font-size:1px;line-height:1px;"> </td>
|
|
204
|
+
<td width="25%" height="3" bgcolor="${config.accentColor}" style="font-size:1px;line-height:1px;"> </td>
|
|
205
|
+
</tr>
|
|
206
|
+
</table>
|
|
207
|
+
|
|
208
|
+
<!-- Footer -->
|
|
209
|
+
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
|
210
|
+
<tr>
|
|
211
|
+
<td width="120" valign="top" style="padding-right: 16px;">
|
|
212
|
+
${logoHtml}
|
|
213
|
+
</td>
|
|
214
|
+
<td valign="top">
|
|
215
|
+
<p style="margin: 0; font-size: 14px; font-weight: 800; color: #000000; letter-spacing: 0.01em;">${escapeHtml(config.brandName)}</p>
|
|
216
|
+
${contactParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 13px; color: #555555; line-height: 1.5;">${contactParts.join(' · ')}</p>` : ''}
|
|
217
|
+
${locationParts.length > 0 ? `<p style="margin: 4px 0 0 0; font-size: 12px; color: #888888; line-height: 1.4;">${locationParts.join(' · ')}</p>` : ''}
|
|
218
|
+
${unsubscribeHtml}
|
|
219
|
+
</td>
|
|
220
|
+
</tr>
|
|
221
|
+
</table>
|
|
222
|
+
`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
interface EmailOptions {
|
|
226
|
+
/** Header background color variant */
|
|
227
|
+
headerColor?: 'primary' | 'secondary'
|
|
228
|
+
/** Preheader text (hidden preview in email clients) */
|
|
229
|
+
preheader?: string
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Wrap email content in the professional template
|
|
234
|
+
*/
|
|
235
|
+
export function emailWrapper(title: string, body: string, options: EmailOptions = {}, config?: EmailTemplateConfig): string {
|
|
236
|
+
const c = resolveConfig(config)
|
|
237
|
+
const { headerColor = 'primary', preheader } = options
|
|
238
|
+
const headerBg = headerColor === 'secondary' ? c.secondaryColor : c.brandColor
|
|
239
|
+
|
|
240
|
+
const badgeHtml = c.brandInitials
|
|
241
|
+
? `<td width="48" align="right" valign="middle">
|
|
242
|
+
<div style="width: 36px; height: 36px; border-radius: 8px; background: #000; display: inline-block; text-align: center; line-height: 36px;">
|
|
243
|
+
<span style="color: #fff; font-weight: 900; font-size: 14px; letter-spacing: 0.05em;">${escapeHtml(c.brandInitials)}</span>
|
|
244
|
+
</div>
|
|
245
|
+
</td>`
|
|
246
|
+
: ''
|
|
247
|
+
|
|
248
|
+
return `<!DOCTYPE html>
|
|
249
|
+
<html lang="fr">
|
|
250
|
+
<head>
|
|
251
|
+
<meta charset="utf-8" />
|
|
252
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
253
|
+
<title>${escapeHtml(title)}</title>
|
|
254
|
+
<!--[if mso]>
|
|
255
|
+
<style type="text/css">
|
|
256
|
+
body, table, td { font-family: Arial, Helvetica, sans-serif; }
|
|
257
|
+
</style>
|
|
258
|
+
<![endif]-->
|
|
259
|
+
</head>
|
|
260
|
+
<body style="margin: 0; padding: 0; background: #f0f0f0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;">
|
|
261
|
+
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">${escapeHtml(preheader)}</div>` : ''}
|
|
262
|
+
|
|
263
|
+
<!-- Outer wrapper -->
|
|
264
|
+
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="background: #f0f0f0; padding: 40px 16px;">
|
|
265
|
+
<tr>
|
|
266
|
+
<td align="center">
|
|
267
|
+
<!-- Main container — wider (660px) -->
|
|
268
|
+
<table cellpadding="0" cellspacing="0" border="0" width="660" style="max-width: 660px; width: 100%;">
|
|
269
|
+
|
|
270
|
+
<!-- Header -->
|
|
271
|
+
<tr>
|
|
272
|
+
<td style="background: ${headerBg}; padding: 32px 40px; border-radius: 12px 12px 0 0; border: 2px solid #000000; border-bottom: none;">
|
|
273
|
+
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
|
274
|
+
<tr>
|
|
275
|
+
<td>
|
|
276
|
+
<h1 style="margin: 0; color: #000000; font-size: 22px; font-weight: 800; line-height: 1.3; letter-spacing: -0.01em;">
|
|
277
|
+
${escapeHtml(title)}
|
|
278
|
+
</h1>
|
|
279
|
+
</td>
|
|
280
|
+
${badgeHtml}
|
|
281
|
+
</tr>
|
|
282
|
+
</table>
|
|
283
|
+
</td>
|
|
284
|
+
</tr>
|
|
285
|
+
|
|
286
|
+
<!-- Body -->
|
|
287
|
+
<tr>
|
|
288
|
+
<td style="background: #ffffff; padding: 40px; border: 2px solid #000000; border-top: none; border-radius: 0 0 12px 12px;">
|
|
289
|
+
${body}
|
|
290
|
+
${emailFooter(c)}
|
|
291
|
+
</td>
|
|
292
|
+
</tr>
|
|
293
|
+
|
|
294
|
+
</table>
|
|
295
|
+
</td>
|
|
296
|
+
</tr>
|
|
297
|
+
</table>
|
|
298
|
+
</body>
|
|
299
|
+
</html>`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Factory ─────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
export interface EmailTemplateFactory {
|
|
305
|
+
emailButton: (text: string, url: string, color?: ButtonColor) => string
|
|
306
|
+
emailWrapper: (title: string, body: string, options?: EmailOptions) => string
|
|
307
|
+
emailQuote: (content: string, borderColor?: string) => string
|
|
308
|
+
emailInfoRow: (label: string, value: string) => string
|
|
309
|
+
emailParagraph: (text: string) => string
|
|
310
|
+
emailTrackingPixel: (ticketId: number | string, messageId?: number | string) => string
|
|
311
|
+
emailRichContent: (html: string) => string
|
|
312
|
+
escapeHtml: (str: string) => string
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Create a pre-configured email template factory.
|
|
317
|
+
* All returned functions use the provided config automatically.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* const email = createEmailTemplateFactory({
|
|
321
|
+
* brandName: 'MyBrand',
|
|
322
|
+
* brandColor: '#FF5733',
|
|
323
|
+
* supportEmail: 'help@mybrand.com',
|
|
324
|
+
* websiteUrl: 'https://mybrand.com',
|
|
325
|
+
* logoUrl: 'https://mybrand.com/logo.png',
|
|
326
|
+
* brandInitials: 'MB',
|
|
327
|
+
* })
|
|
328
|
+
*
|
|
329
|
+
* const html = email.emailWrapper('Welcome!', email.emailParagraph('Hello world.'))
|
|
330
|
+
*/
|
|
331
|
+
export function createEmailTemplateFactory(config: EmailTemplateConfig): EmailTemplateFactory {
|
|
332
|
+
const c = resolveConfig(config)
|
|
333
|
+
return {
|
|
334
|
+
emailButton: (text, url, color = 'primary') => emailButton(text, url, color, c),
|
|
335
|
+
emailWrapper: (title, body, options = {}) => emailWrapper(title, body, options, c),
|
|
336
|
+
emailQuote: (content, borderColor?) => emailQuote(content, borderColor, c),
|
|
337
|
+
emailInfoRow,
|
|
338
|
+
emailParagraph,
|
|
339
|
+
emailTrackingPixel: (ticketId, messageId?) => emailTrackingPixel(ticketId, messageId, c.websiteUrl),
|
|
340
|
+
emailRichContent: (html) => emailRichContent(html, c),
|
|
341
|
+
escapeHtml,
|
|
342
|
+
}
|
|
343
|
+
}
|