@dev.smartpricing/message-composer-layer 1.0.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.
Files changed (169) hide show
  1. package/README.md +75 -0
  2. package/app/api/ai.ts +28 -0
  3. package/app/api/brands.ts +54 -0
  4. package/app/api/client.ts +56 -0
  5. package/app/api/compilation.ts +34 -0
  6. package/app/api/dynamicValues.ts +11 -0
  7. package/app/api/index.ts +11 -0
  8. package/app/api/languages.ts +6 -0
  9. package/app/api/media.ts +26 -0
  10. package/app/api/messageGroups.ts +49 -0
  11. package/app/api/messages.ts +22 -0
  12. package/app/api/meta.ts +18 -0
  13. package/app/api/tracking.ts +9 -0
  14. package/app/components/Brand/BrandDrawer.vue +281 -0
  15. package/app/components/Brand/BrandPreview.vue +137 -0
  16. package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
  17. package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
  18. package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
  19. package/app/components/Common/BrandColorSwatches.vue +37 -0
  20. package/app/components/Common/ColorInput.vue +144 -0
  21. package/app/components/Common/EmojiPickerButton.vue +35 -0
  22. package/app/components/Common/FontFamilySelect.vue +39 -0
  23. package/app/components/Common/HtmlPreviewIframe.vue +68 -0
  24. package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
  25. package/app/components/Common/LanguageList.vue +356 -0
  26. package/app/components/Common/LoadingOverlay.vue +12 -0
  27. package/app/components/Common/MediaUploadOrSelect.vue +299 -0
  28. package/app/components/Common/MessagePreviewSlideover.vue +88 -0
  29. package/app/components/Common/MonacoEditor.vue +110 -0
  30. package/app/components/Common/PhoneInput.vue +804 -0
  31. package/app/components/Common/SectionHeader.vue +26 -0
  32. package/app/components/Common/TranslationActionBar.vue +139 -0
  33. package/app/components/Common/ViewLessMore.vue +40 -0
  34. package/app/components/CreateTemplateButton.vue +59 -0
  35. package/app/components/DynamicDataButton.vue +75 -0
  36. package/app/components/DynamicDataSlideover.vue +149 -0
  37. package/app/components/Editor/AiGenerateSlideover.vue +292 -0
  38. package/app/components/Editor/EditorLinkPopover.vue +154 -0
  39. package/app/components/Editor/RichTextEditor.vue +495 -0
  40. package/app/components/Email/Editor/BlockLibrary.vue +89 -0
  41. package/app/components/Email/Editor/BlockSettings.vue +60 -0
  42. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
  43. package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
  44. package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
  45. package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
  46. package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
  47. package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
  48. package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
  49. package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
  50. package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
  51. package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
  52. package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
  53. package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
  54. package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
  55. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
  56. package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
  57. package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
  58. package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
  59. package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
  60. package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
  61. package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
  62. package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
  63. package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
  64. package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
  65. package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
  66. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
  67. package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
  68. package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
  69. package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
  70. package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
  71. package/app/components/Email/Editor/Canvas.vue +143 -0
  72. package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
  73. package/app/components/Email/Editor/Index.vue +77 -0
  74. package/app/components/Email/Editor/LeftSidebar.vue +95 -0
  75. package/app/components/Email/InlinePreview.vue +124 -0
  76. package/app/components/Email/Preview.vue +126 -0
  77. package/app/components/Email/PreviewPanel.vue +57 -0
  78. package/app/components/Email/SendTestEmailModal.vue +121 -0
  79. package/app/components/Email/TranslationEditor.vue +198 -0
  80. package/app/components/Email/Translator/Button.vue +101 -0
  81. package/app/components/Email/Translator/Discount.vue +60 -0
  82. package/app/components/Email/Translator/Footer.vue +66 -0
  83. package/app/components/Email/Translator/Grid.vue +127 -0
  84. package/app/components/Email/Translator/Header.vue +90 -0
  85. package/app/components/Email/Translator/Heading.vue +66 -0
  86. package/app/components/Email/Translator/Image.vue +91 -0
  87. package/app/components/Email/Translator/Index.vue +138 -0
  88. package/app/components/Email/Translator/Paragraph.vue +66 -0
  89. package/app/components/Email/Translator/RawHtml.vue +64 -0
  90. package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
  91. package/app/components/TemplateList/BaseTable.vue +354 -0
  92. package/app/components/TemplateList/StatusCell.vue +67 -0
  93. package/app/components/Whatsapp/CategoryChanger.vue +63 -0
  94. package/app/components/Whatsapp/Editor/Body.vue +53 -0
  95. package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
  96. package/app/components/Whatsapp/Editor/Footer.vue +70 -0
  97. package/app/components/Whatsapp/Editor/Header.vue +156 -0
  98. package/app/components/Whatsapp/Editor/Index.vue +89 -0
  99. package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
  100. package/app/components/Whatsapp/Preview.vue +197 -0
  101. package/app/components/Whatsapp/PreviewPanel.vue +24 -0
  102. package/app/components/Whatsapp/TranslationEditor.vue +150 -0
  103. package/app/components/Whatsapp/Translator/Body.vue +61 -0
  104. package/app/components/Whatsapp/Translator/Button.vue +209 -0
  105. package/app/components/Whatsapp/Translator/Footer.vue +56 -0
  106. package/app/components/Whatsapp/Translator/Header.vue +61 -0
  107. package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
  108. package/app/components/Whatsapp/Translator/Index.vue +159 -0
  109. package/app/composables/composerContext.ts +35 -0
  110. package/app/composables/useAutoTranslate.ts +242 -0
  111. package/app/composables/useAutoTranslateEmail.ts +294 -0
  112. package/app/composables/useChangeTracking.ts +212 -0
  113. package/app/composables/useChangeTrackingEmail.ts +227 -0
  114. package/app/composables/useChannels.ts +36 -0
  115. package/app/composables/useEmailBlockContext.ts +43 -0
  116. package/app/composables/useExitConfirmation.ts +39 -0
  117. package/app/composables/useGoBack.ts +15 -0
  118. package/app/composables/useIncompleteTranslations.ts +55 -0
  119. package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
  120. package/app/composables/useMediaLimits.ts +42 -0
  121. package/app/composables/useTemplateFormValidation.ts +109 -0
  122. package/app/composables/useTemplateOperations.ts +147 -0
  123. package/app/composables/useTracking.ts +39 -0
  124. package/app/composables/useTranslationFilters.ts +176 -0
  125. package/app/composables/useTranslationFiltersEmail.ts +244 -0
  126. package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
  127. package/app/middleware/channel-guard.ts +13 -0
  128. package/app/pages/brands.vue +195 -0
  129. package/app/pages/images.vue +141 -0
  130. package/app/pages/index.vue +198 -0
  131. package/app/pages/preview/[id].vue +47 -0
  132. package/app/pages/template/email/[id].vue +596 -0
  133. package/app/pages/template/email/create.vue +562 -0
  134. package/app/pages/template/whatsapp/[id].vue +561 -0
  135. package/app/pages/template/whatsapp/create.vue +468 -0
  136. package/app/plugins/admin-shortcut.client.ts +7 -0
  137. package/app/plugins/tracking.client.ts +12 -0
  138. package/app/queries/ai.ts +20 -0
  139. package/app/queries/brands.ts +75 -0
  140. package/app/queries/compilation.ts +37 -0
  141. package/app/queries/dynamicValues.ts +13 -0
  142. package/app/queries/index.ts +9 -0
  143. package/app/queries/languages.ts +10 -0
  144. package/app/queries/media.ts +27 -0
  145. package/app/queries/messageGroups.ts +112 -0
  146. package/app/queries/messages.ts +42 -0
  147. package/app/queries/meta.ts +13 -0
  148. package/app/stores/adminStore.ts +50 -0
  149. package/app/stores/appContext.ts +99 -0
  150. package/app/stores/emailEditor.ts +220 -0
  151. package/app/stores/main.ts +3 -0
  152. package/app/stores/metaSubmission.ts +116 -0
  153. package/app/types/app-config.d.ts +14 -0
  154. package/app/types/hooks.d.ts +9 -0
  155. package/app/types/tracking.ts +148 -0
  156. package/app/utils/locale.ts +17 -0
  157. package/app/utils/notifyParent.ts +7 -0
  158. package/app/utils/resolveDefaultLanguage.ts +14 -0
  159. package/app/utils/socialDefaults.ts +42 -0
  160. package/app/utils/testIds.const.ts +728 -0
  161. package/app.config.ts +5 -0
  162. package/i18n/check-translations.ts +56 -0
  163. package/i18n/locales/de.ts +834 -0
  164. package/i18n/locales/en.ts +826 -0
  165. package/i18n/locales/es.ts +833 -0
  166. package/i18n/locales/fr.ts +837 -0
  167. package/i18n/locales/it.ts +831 -0
  168. package/nuxt.config.ts +65 -0
  169. package/package.json +64 -0
@@ -0,0 +1,468 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({
3
+ middleware: ['channel-guard'],
4
+ })
5
+
6
+ import WhatsappEditorSyncWrapper from '@/components/Whatsapp/Editor/SyncWrapper.vue'
7
+ import WhatsappPreview from '@/components/Whatsapp/PreviewPanel.vue'
8
+ import TranslationEditor from '@/components/Whatsapp/TranslationEditor.vue'
9
+
10
+ import IncompleteTranslationsDialog from '@/components/Common/IncompleteTranslationsDialog.vue'
11
+
12
+ import CategoryChanger from '@/components/Whatsapp/CategoryChanger.vue'
13
+ import { computed, ref, watch } from 'vue'
14
+ import {
15
+ type MessageGroup,
16
+ type MessageGroupCategory,
17
+ type PostEntity,
18
+ type WhatsappMessage,
19
+ type LanguageFromDb,
20
+ } from '@dev.smartpricing/message-composer-utils/types'
21
+ import { getWhatsappEmpty } from '@dev.smartpricing/message-composer-utils/utils'
22
+ import LanguageList from '@/components/Common/LanguageList.vue'
23
+ import { useMetaSubmissionStore } from '@/stores/metaSubmission'
24
+ import { useLanguagesQuery } from '~/queries'
25
+ import { useAppContextStore } from '@/stores/appContext'
26
+ import type { WaHeaderType } from '~/types/tracking'
27
+
28
+ const { trackEvent } = useTracking()
29
+ const { mainLanguage } = storeToRefs(useAppContextStore())
30
+
31
+ const adminStore = useAdminStore()
32
+
33
+ const name = ref('')
34
+ const category = ref<MessageGroupCategory>('marketing')
35
+ const isInternal = ref(false)
36
+ const language = ref(mainLanguage.value)
37
+ const { goBackOrFallback } = useGoBack()
38
+
39
+ const activeTab = ref<'content' | 'translations'>('content')
40
+ const baseLanguage = ref(mainLanguage.value)
41
+ const targetLanguage = ref(mainLanguage.value)
42
+
43
+ // Create temporary draft ID for change tracking
44
+ const draftId = ref(`draft-${Date.now()}`)
45
+ const changeTracking = useChangeTracking(draftId)
46
+
47
+ // Store original messageGroup values (empty for new message group)
48
+ const originalMessageGroupName = ref('')
49
+ const originalMessageGroupCategory = ref<MessageGroupCategory>('marketing')
50
+
51
+ const { saving, createTemplate } = useTemplateOperations({
52
+ onSuccess: () => {
53
+ created.value = true
54
+ changeTracking.clearAllChanges()
55
+ goBackOrFallback()
56
+ },
57
+ })
58
+
59
+ const { submitToMeta } = useMetaSubmissionStore()
60
+
61
+ const messageGroup = computed(() => {
62
+ return {
63
+ name: name.value,
64
+ category: category.value,
65
+ visibility: isInternal.value ? 'internal' : 'public',
66
+ } satisfies PostEntity<MessageGroup>
67
+ })
68
+ const messages = ref<Record<string, PostEntity<WhatsappMessage>>>({
69
+ [mainLanguage.value]: getWhatsappEmpty(mainLanguage.value),
70
+ })
71
+
72
+ const { formState, draftSchema, validationErrors, validateDraft, validateFull, clearErrors } =
73
+ useTemplateFormValidation({
74
+ type: 'whatsapp',
75
+ name,
76
+ category,
77
+ messages,
78
+ })
79
+
80
+ // Record original state for initial mainLanguage
81
+ if (messages.value[mainLanguage.value]) {
82
+ changeTracking.recordOriginalState(
83
+ mainLanguage.value,
84
+ messages.value[mainLanguage.value] as WhatsappMessage,
85
+ )
86
+ }
87
+
88
+ // Initialize WhatsApp message group behaviors composable
89
+ const { ensureUnsubscribeButton } = useWhatsappTemplateBehaviors({
90
+ category,
91
+ messages: messages,
92
+ })
93
+ ensureUnsubscribeButton()
94
+
95
+ const created = ref(false)
96
+
97
+ const hasAnyChanges = computed(() => {
98
+ // Check if message group name or category changed
99
+ const nameChanged = name.value !== originalMessageGroupName.value
100
+ const categoryChanged = category.value !== originalMessageGroupCategory.value
101
+ if (nameChanged || categoryChanged) return true
102
+
103
+ // Check if any language has changes
104
+ for (const languageId in messages.value) {
105
+ const message = messages.value[languageId]
106
+ if (message && changeTracking.hasChanges(languageId, message as WhatsappMessage)) {
107
+ return true
108
+ }
109
+ }
110
+ return false
111
+ })
112
+
113
+ useExitConfirmation(computed(() => hasAnyChanges.value && !created.value))
114
+
115
+ // ── Tracking helpers ──
116
+ function getWaBaseProps() {
117
+ return { name: name.value, category: category.value }
118
+ }
119
+
120
+ function getWaDraftProps() {
121
+ const msg = messages.value[mainLanguage.value]
122
+ return {
123
+ ...getWaBaseProps(),
124
+ languages: Object.keys(messages.value),
125
+ header: (msg?.body?.header?.type ?? 'none') as WaHeaderType,
126
+ footer: !!msg?.body?.footer?.enabled,
127
+ buttons: msg?.body?.buttons?.buttons?.length ?? 0,
128
+ is_internal: isInternal.value,
129
+ }
130
+ }
131
+
132
+ watch(activeTab, (tab) => {
133
+ trackEvent('wa_create.change_tab', { ...getWaBaseProps(), tab })
134
+ })
135
+
136
+ function onAddLanguage(lang: string) {
137
+ trackEvent('wa_create.add_language', { ...getWaBaseProps(), language: lang })
138
+ }
139
+
140
+ function onRemoveLanguage(lang: string) {
141
+ trackEvent('wa_create.remove_language', {
142
+ ...getWaBaseProps(),
143
+ language: lang,
144
+ })
145
+ }
146
+
147
+ function onAutoTranslateTracked(lang: string) {
148
+ trackEvent('wa_create.auto_translate', {
149
+ ...getWaBaseProps(),
150
+ language: lang,
151
+ })
152
+ }
153
+
154
+ function onPreview(lang: string) {
155
+ trackEvent('wa_create.preview', { ...getWaBaseProps(), language: lang })
156
+ }
157
+
158
+ function onFiltersChanged(payload: {
159
+ language: string
160
+ search_term: string
161
+ show_only_missing: boolean
162
+ show_only_changes: boolean
163
+ }) {
164
+ trackEvent('wa_create.translations_filters', { ...getWaBaseProps(), ...payload })
165
+ }
166
+
167
+ // Get languages for incomplete translations check
168
+ const { data: languages } = useLanguagesQuery()
169
+
170
+ // Check for incomplete translations
171
+ const { incompleteLanguages, hasIncompleteTranslations } = useIncompleteTranslations({
172
+ messages,
173
+ languages,
174
+ })
175
+
176
+ // Dialog state and pending action
177
+ const showIncompleteDialog = ref(false)
178
+ const pendingAction = ref<'save_draft' | 'request_approval' | null>(null)
179
+
180
+ // Auto-translate composable
181
+ const { translateMessage, isTranslating } = useAutoTranslate()
182
+
183
+ async function saveDraft() {
184
+ trackEvent('wa_create.save_draft', getWaDraftProps())
185
+ const templateData = {
186
+ messageGroup: messageGroup.value,
187
+ messages: messages.value,
188
+ }
189
+ const result = await createTemplate(templateData, 'draft')
190
+ if (result) {
191
+ notifyParent('TEMPLATE_SAVED_DRAFT', { id: result.id, name: result.name })
192
+ }
193
+ }
194
+
195
+ async function requestApproval() {
196
+ trackEvent('wa_create.request_approval', getWaDraftProps())
197
+ const templateData = {
198
+ messageGroup: messageGroup.value,
199
+ messages: messages.value,
200
+ }
201
+ const result = await createTemplate(templateData, 'draft')
202
+
203
+ if (result) {
204
+ await submitToMeta(result.id.toString())
205
+ notifyParent('TEMPLATE_APPROVAL_REQUESTED', { id: result.id, name: result.name })
206
+ }
207
+ }
208
+
209
+ // Wrapper functions to validate then check for incomplete translations
210
+ async function handleSaveDraft() {
211
+ if (!(await validateDraft())) return
212
+ if (hasIncompleteTranslations.value) {
213
+ pendingAction.value = 'save_draft'
214
+ showIncompleteDialog.value = true
215
+ } else {
216
+ saveDraft()
217
+ }
218
+ }
219
+
220
+ async function handleRequestApproval() {
221
+ if (!(await validateFull())) return
222
+ if (hasIncompleteTranslations.value) {
223
+ pendingAction.value = 'request_approval'
224
+ showIncompleteDialog.value = true
225
+ } else {
226
+ requestApproval()
227
+ }
228
+ }
229
+
230
+ // Proceed with save without translating
231
+ function proceedWithSave() {
232
+ if (pendingAction.value === 'save_draft') {
233
+ saveDraft()
234
+ } else if (pendingAction.value === 'request_approval') {
235
+ requestApproval()
236
+ }
237
+ pendingAction.value = null
238
+ }
239
+
240
+ // Go to translations tab
241
+ function goToTranslations() {
242
+ activeTab.value = 'translations'
243
+ pendingAction.value = null
244
+ }
245
+
246
+ // Translate automatically and save
247
+ async function translateAndSave() {
248
+ if (!incompleteLanguages.value.length) {
249
+ proceedWithSave()
250
+ return
251
+ }
252
+
253
+ const baseMessage = messages.value[baseLanguage.value]
254
+ if (!baseMessage) {
255
+ proceedWithSave()
256
+ return
257
+ }
258
+
259
+ // Translate all incomplete languages
260
+ for (const { language: lang } of incompleteLanguages.value) {
261
+ const targetMessage = messages.value[lang.id]
262
+ if (!targetMessage) continue
263
+
264
+ await translateMessage(
265
+ baseMessage as WhatsappMessage,
266
+ targetMessage as WhatsappMessage,
267
+ baseLanguage.value,
268
+ lang.id,
269
+ )
270
+ }
271
+
272
+ // After translation, proceed with save
273
+ proceedWithSave()
274
+ }
275
+ </script>
276
+
277
+ <template>
278
+ <SNavigationPage>
279
+ <template #header>
280
+ <SNavigationBarHeader
281
+ :title="$t('pages.whatsapp.create_message_group.title')"
282
+ :tabs="[
283
+ { label: $t('pages.whatsapp.tabs.content'), value: 'content' },
284
+ { label: $t('pages.whatsapp.tabs.translations'), value: 'translations' },
285
+ ]"
286
+ :active-tab="activeTab"
287
+ :data-testid="templateWhatsappCreateTestIds.navbar"
288
+ @tab-change="activeTab = $event"
289
+ >
290
+ <template #actions>
291
+ <UButton
292
+ @click="
293
+ () => {
294
+ notifyParent('TEMPLATE_CANCELLED')
295
+ goBackOrFallback()
296
+ }
297
+ "
298
+ :label="$t('common.actions.cancel')"
299
+ color="primary"
300
+ variant="ghost"
301
+ :loading="saving || isTranslating"
302
+ :data-testid="templateWhatsappCreateTestIds.cancelButton"
303
+ />
304
+ <UButton
305
+ @click="handleSaveDraft()"
306
+ :label="$t('pages.whatsapp.create_message_group.save_draft')"
307
+ color="primary"
308
+ variant="outline"
309
+ :loading="saving || isTranslating"
310
+ :data-testid="templateWhatsappCreateTestIds.saveDraftButton"
311
+ />
312
+ <UButton
313
+ @click="handleRequestApproval()"
314
+ :label="$t('pages.whatsapp.create_message_group.request_approval')"
315
+ color="primary"
316
+ :loading="saving || isTranslating"
317
+ :data-testid="templateWhatsappCreateTestIds.requestApprovalButton"
318
+ />
319
+ </template>
320
+ </SNavigationBarHeader>
321
+ </template>
322
+
323
+ <div v-if="validationErrors.length">
324
+ <UAlert
325
+ color="error"
326
+ variant="subtle"
327
+ icon="i-lucide-alert-triangle"
328
+ :title="$t('editor.validation.error.title')"
329
+ :close="true"
330
+ @update:open="clearErrors()"
331
+ :data-testid="templateWhatsappCreateTestIds.validationAlert"
332
+ >
333
+ <template #description>
334
+ <ul class="list-disc pl-4 space-y-1">
335
+ <li v-for="err in validationErrors" :key="err">
336
+ {{ err }}
337
+ </li>
338
+ </ul>
339
+ </template>
340
+ </UAlert>
341
+ </div>
342
+
343
+ <UForm
344
+ ref="templateForm"
345
+ :schema="draftSchema"
346
+ :state="formState"
347
+ :validate-on="[]"
348
+ class="flex-1 relative min-h-0 flex flex-col"
349
+ >
350
+ <UCard
351
+ v-if="activeTab === 'content'"
352
+ :ui="{
353
+ root: 'flex flex-col',
354
+ body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
355
+ header: 'sm:px-4 px-4 py-4 sm:py-4',
356
+ }"
357
+ :data-testid="templateWhatsappCreateTestIds.contentCard"
358
+ >
359
+ <template #header>
360
+ <div class="grid grid-cols-3 gap-2">
361
+ <UFieldGroup>
362
+ <UBadge variant="outline" color="neutral" size="lg">{{
363
+ $t('pages.whatsapp.create_message_group.name')
364
+ }}</UBadge>
365
+ <UInput
366
+ v-model="name"
367
+ :placeholder="$t('pages.whatsapp.create_message_group.name_placeholder')"
368
+ :data-testid="templateWhatsappCreateTestIds.nameInput"
369
+ />
370
+ </UFieldGroup>
371
+
372
+ <UFieldGroup>
373
+ <UBadge variant="outline" color="neutral" size="lg">{{
374
+ $t('pages.whatsapp.create_message_group.category')
375
+ }}</UBadge>
376
+ <CategoryChanger v-model:category="category" @changed="ensureUnsubscribeButton" />
377
+ </UFieldGroup>
378
+
379
+ <USwitch
380
+ v-model="isInternal"
381
+ :label="$t('common.internal')"
382
+ v-if="adminStore.isAdminModeActive"
383
+ color="info"
384
+ :data-testid="templateWhatsappCreateTestIds.internalSwitch"
385
+ />
386
+ <!-- <DocumentationPanel
387
+ :category="$t('documentation.whatsapp_category.category')"
388
+ :title="$t('documentation.whatsapp_category.title')"
389
+ :description="$t('documentation.whatsapp_category.description')"
390
+ :info="{
391
+ title: '',
392
+ text: $t('documentation.whatsapp_category.info'),
393
+ }"
394
+ :content="[
395
+ $t('documentation.whatsapp_category.content.marketing'),
396
+ $t('documentation.whatsapp_category.content.utility'),
397
+ ]"
398
+ /> -->
399
+ </div>
400
+ </template>
401
+
402
+ <div class="grid grid-cols-3 divide-x divide-muted overflow-auto max-h-full h-full">
403
+ <div class="col-span-2 overflow-y-auto pt-4 pb-4" v-if="messages[language]">
404
+ <h2 class="px-4 pb-4 label-lg">
405
+ {{ $t('pages.whatsapp.create_message_group.message_content') }}
406
+ </h2>
407
+ <WhatsappEditorSyncWrapper
408
+ v-model="messages[language]"
409
+ :messages="messages"
410
+ :main-language-id="language"
411
+ :key="language"
412
+ />
413
+ </div>
414
+ <div class="col-span-1 overflow-y-auto" v-if="messages[language]">
415
+ <WhatsappPreview :message="messages[language]" />
416
+ </div>
417
+ </div>
418
+ </UCard>
419
+
420
+ <UCard
421
+ v-if="activeTab === 'translations'"
422
+ :ui="{
423
+ root: 'flex flex-col h-full',
424
+ body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
425
+ header: 'sm:px-4 px-4 py-4 sm:py-4',
426
+ }"
427
+ :data-testid="templateWhatsappCreateTestIds.translationsCard"
428
+ >
429
+ <div
430
+ class="grid grid-cols-[auto_1fr] divide-x divide-muted overflow-auto max-h-full h-full"
431
+ >
432
+ <div class="overflow-y-auto">
433
+ <LanguageList
434
+ v-model:messages="messages"
435
+ v-model:language="targetLanguage"
436
+ render-type="whatsapp_v1"
437
+ :message-group-id="draftId"
438
+ @add-language="onAddLanguage"
439
+ @remove-language="onRemoveLanguage"
440
+ :main-language-id="baseLanguage"
441
+ />
442
+ </div>
443
+ <div class="overflow-y-auto" v-if="messages[baseLanguage] && messages[targetLanguage]">
444
+ <TranslationEditor
445
+ :base-message="messages[baseLanguage]"
446
+ v-model:target-message="messages[targetLanguage]"
447
+ :messages="messages"
448
+ :current-language="targetLanguage"
449
+ :base-language="baseLanguage"
450
+ :message-group-id="draftId"
451
+ @auto-translate-tracked="onAutoTranslateTracked"
452
+ @preview="onPreview"
453
+ @filters-changed="onFiltersChanged"
454
+ />
455
+ </div>
456
+ </div>
457
+ </UCard>
458
+ </UForm>
459
+
460
+ <IncompleteTranslationsDialog
461
+ v-model="showIncompleteDialog"
462
+ :incomplete-languages="incompleteLanguages"
463
+ @save-without-translating="proceedWithSave"
464
+ @translate-and-save="translateAndSave"
465
+ @go-to-translations="goToTranslations"
466
+ />
467
+ </SNavigationPage>
468
+ </template>
@@ -0,0 +1,7 @@
1
+ export default defineNuxtPlugin(() => {
2
+ onNuxtReady(() => {
3
+ const adminStore = useAdminStore()
4
+ const { shift_space } = useMagicKeys()
5
+ whenever(shift_space!, () => adminStore.toggleAdminMode())
6
+ })
7
+ })
@@ -0,0 +1,12 @@
1
+ export default defineNuxtPlugin(() => {
2
+ const appConfig = useAppConfig()
3
+ if (!appConfig.composer?.tracking) return
4
+
5
+ const { trackPageView } = useTracking()
6
+ const router = useRouter()
7
+
8
+ router.afterEach((to) => {
9
+ const pageName = (to.name as string) || to.path
10
+ trackPageView(pageName)
11
+ })
12
+ })
@@ -0,0 +1,20 @@
1
+ import { useMutation } from '@pinia/colada'
2
+ import { aiApi } from '~/api'
3
+
4
+ export function useRefineTextMutation() {
5
+ return useMutation({
6
+ mutation: aiApi.refineText,
7
+ })
8
+ }
9
+
10
+ export function useTranslateMutation() {
11
+ return useMutation({
12
+ mutation: aiApi.translate,
13
+ })
14
+ }
15
+
16
+ export function useGenerateTextMutation() {
17
+ return useMutation({
18
+ mutation: aiApi.generateText,
19
+ })
20
+ }
@@ -0,0 +1,75 @@
1
+ import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
2
+ import type {
3
+ BrandCreateBody,
4
+ BrandUpdateBody,
5
+ } from '@dev.smartpricing/message-composer-utils/types'
6
+ import { brandsApi, propertiesApi } from '~/api'
7
+
8
+ // QUERIES
9
+ export function useBrandsQuery(options?: {
10
+ sort_by?: 'created_at' | 'name'
11
+ direction?: 'asc' | 'desc'
12
+ }) {
13
+ return useQuery({
14
+ key: () => ['brands', options?.sort_by, options?.direction],
15
+ query: () => brandsApi.getAll(options),
16
+ staleTime: 2 * 60 * 1000,
17
+ })
18
+ }
19
+
20
+ export function useBrandQuery(id: MaybeRefOrGetter<string>) {
21
+ return useQuery({
22
+ key: () => ['brands', toValue(id)],
23
+ query: () => brandsApi.getById(toValue(id)),
24
+ staleTime: 2 * 60 * 1000,
25
+ })
26
+ }
27
+
28
+ export function usePropertiesQuery() {
29
+ return useQuery({
30
+ key: () => ['properties'],
31
+ query: () => propertiesApi.getAll(),
32
+ staleTime: 10 * 60 * 1000,
33
+ })
34
+ }
35
+
36
+ // MUTATIONS
37
+ export function useCreateBrandMutation() {
38
+ const queryCache = useQueryCache()
39
+
40
+ return useMutation({
41
+ mutation: (data: BrandCreateBody) => brandsApi.create(data),
42
+ onSuccess: () => {
43
+ queryCache.invalidateQueries({ key: ['brands'] })
44
+ },
45
+ })
46
+ }
47
+
48
+ export function useUpdateBrandMutation() {
49
+ const queryCache = useQueryCache()
50
+
51
+ return useMutation({
52
+ mutation: ({ id, data }: { id: string; data: BrandUpdateBody }) => brandsApi.update(id, data),
53
+ onSuccess: (_, { id }) => {
54
+ queryCache.invalidateQueries({ key: ['brands', id] })
55
+ queryCache.invalidateQueries({ key: ['brands'] })
56
+ },
57
+ })
58
+ }
59
+
60
+ export function useDeleteBrandMutation() {
61
+ const queryCache = useQueryCache()
62
+
63
+ return useMutation({
64
+ mutation: brandsApi.delete,
65
+ onSuccess: () => {
66
+ queryCache.invalidateQueries({ key: ['brands'] })
67
+ },
68
+ })
69
+ }
70
+
71
+ export function useBrandPreviewMutation() {
72
+ return useMutation({
73
+ mutation: brandsApi.preview,
74
+ })
75
+ }
@@ -0,0 +1,37 @@
1
+ import { useMutation, useQuery } from '@pinia/colada'
2
+ import type { MaybeRefOrGetter } from 'vue'
3
+ import { toValue } from 'vue'
4
+ import { compilationApi } from '~/api'
5
+ import type {
6
+ CompileMessageRequest,
7
+ PreviewMessageRequest,
8
+ } from '@dev.smartpricing/message-composer-utils/types'
9
+
10
+ export function useSendTestEmailMutation() {
11
+ return useMutation({
12
+ mutation: compilationApi.sendTestEmail,
13
+ })
14
+ }
15
+
16
+ export function useEmailParametersQuery(
17
+ messageGroupId: MaybeRefOrGetter<string>,
18
+ language: MaybeRefOrGetter<string>,
19
+ ) {
20
+ return useQuery({
21
+ key: () => ['email-parameters', toValue(messageGroupId), toValue(language)],
22
+ query: () => compilationApi.getParameters(toValue(messageGroupId), toValue(language)),
23
+ })
24
+ }
25
+
26
+ export function useCompileEmailMutation() {
27
+ return useMutation({
28
+ mutation: ({ messageGroupId, data }: { messageGroupId: string; data: CompileMessageRequest }) =>
29
+ compilationApi.compileEmail(messageGroupId, data),
30
+ })
31
+ }
32
+
33
+ export function usePreviewEmailMutation() {
34
+ return useMutation({
35
+ mutation: (data: PreviewMessageRequest) => compilationApi.previewEmail(data),
36
+ })
37
+ }
@@ -0,0 +1,13 @@
1
+ import { useQuery } from '@pinia/colada'
2
+ import { dynamicValuesApi } from '~/api'
3
+ import { useAppContextStore } from '~/stores/appContext'
4
+
5
+ export function useDynamicValuesQuery() {
6
+ const { product, mainLanguage } = storeToRefs(useAppContextStore())
7
+
8
+ return useQuery({
9
+ key: () => ['dynamic-values', product.value, mainLanguage.value],
10
+ query: () => dynamicValuesApi.get(product.value || 'browser', mainLanguage.value || 'en'),
11
+ staleTime: 30 * 60 * 1000,
12
+ })
13
+ }
@@ -0,0 +1,9 @@
1
+ export * from './languages'
2
+ export * from './messageGroups'
3
+ export * from './messages'
4
+ export * from './media'
5
+ export * from './ai'
6
+ export * from './meta'
7
+ export * from './compilation'
8
+ export * from './dynamicValues'
9
+ export * from './brands'
@@ -0,0 +1,10 @@
1
+ import { useQuery } from '@pinia/colada'
2
+ import { languagesApi } from '~/api'
3
+
4
+ export function useLanguagesQuery() {
5
+ return useQuery({
6
+ key: ['languages'],
7
+ query: languagesApi.getAll,
8
+ staleTime: 10 * 60 * 1000, // 10 minutes
9
+ })
10
+ }
@@ -0,0 +1,27 @@
1
+ import { useMutation, useQuery } from '@pinia/colada'
2
+ import { mediaApi } from '~/api'
3
+
4
+ export function useMediaQuery() {
5
+ return useQuery({
6
+ key: ['media'],
7
+ query: mediaApi.list,
8
+ })
9
+ }
10
+
11
+ export function useUploadMediaMutation() {
12
+ return useMutation({
13
+ mutation: mediaApi.upload,
14
+ })
15
+ }
16
+
17
+ export function useUploadToSmartchatMutation() {
18
+ return useMutation({
19
+ mutation: mediaApi.uploadToSmartchat,
20
+ })
21
+ }
22
+
23
+ export function useDeleteMediaMutation() {
24
+ return useMutation({
25
+ mutation: mediaApi.delete,
26
+ })
27
+ }