@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,198 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({
3
+ name: 'message-library',
4
+ })
5
+
6
+ import {
7
+ useMessageGroupsQuery,
8
+ useDeleteMessageGroupMutation,
9
+ usePublishMessageGroupMutation,
10
+ useUnpublishMessageGroupMutation,
11
+ } from '~/queries'
12
+ import type {
13
+ MessageGroupWithMessageSummary,
14
+ RenderType,
15
+ } from '@dev.smartpricing/message-composer-utils/types'
16
+ import type { MessageActionProperties, MessagesKind } from '@/types/tracking'
17
+ import { ref, watch, computed } from 'vue'
18
+ import { useRouter } from 'vue-router'
19
+ import BaseTable from '@/components/TemplateList/BaseTable.vue'
20
+ import CreateTemplateButton from '@/components/CreateTemplateButton.vue'
21
+
22
+ const { hasWhatsapp, hasEmail, defaultRenderType, isRenderTypeAllowed } = useChannels()
23
+ const { trackEvent } = useTracking()
24
+
25
+ // Load render type from session or default to first allowed
26
+ const RENDER_TYPE_KEY = 'template_render_type'
27
+ const storedRenderType = sessionStorage.getItem(RENDER_TYPE_KEY) as RenderType | null
28
+ const initialRenderType =
29
+ storedRenderType && isRenderTypeAllowed(storedRenderType)
30
+ ? storedRenderType
31
+ : defaultRenderType.value
32
+ const renderType = ref<RenderType>(initialRenderType)
33
+
34
+ // Persist render type to session when it changes
35
+ watch(renderType, (newType) => {
36
+ sessionStorage.setItem(RENDER_TYPE_KEY, newType)
37
+ trackEvent('change_tab', { messages_kind: newType === 'email_v1' ? 'email' : 'whatsapp' })
38
+ })
39
+ const {
40
+ data: messageGroups,
41
+ isPending: isFetching,
42
+ refetch: execute,
43
+ } = useMessageGroupsQuery(computed(() => ({ renderType: renderType.value })))
44
+
45
+ // Manual polling (Pinia Colada doesn't support automatic polling yet)
46
+ const interval = setInterval(() => {
47
+ execute()
48
+ }, 15000)
49
+
50
+ onBeforeUnmount(() => {
51
+ clearInterval(interval)
52
+ })
53
+
54
+ const { confirm } = useConfirm()
55
+ const { mutateAsync: deleteGroup } = useDeleteMessageGroupMutation()
56
+ const { mutateAsync: publishGroup } = usePublishMessageGroupMutation()
57
+ const { mutateAsync: unpublishGroup } = useUnpublishMessageGroupMutation()
58
+
59
+ function getMessageTrackingProps(row: MessageGroupWithMessageSummary): MessageActionProperties {
60
+ return {
61
+ message_name: row.name,
62
+ languages: row.messages.map((m) => m.language),
63
+ message_id: row.id,
64
+ message_kind: (row.messages[0]?.render_type === 'email_v1'
65
+ ? 'email'
66
+ : 'whatsapp') as MessagesKind,
67
+ message_category: row.category,
68
+ }
69
+ }
70
+
71
+ function askDeleteGroup(row: MessageGroupWithMessageSummary) {
72
+ confirm({
73
+ title: t('dialogs.delete_message_group.title'),
74
+ message: t('dialogs.delete_message_group.message', { name: row.name }),
75
+ destructive: true,
76
+ confirmLabel: t('common.actions.delete'),
77
+ cancelLabel: t('common.actions.cancel'),
78
+ action: async () => {
79
+ await deleteGroup(row.id)
80
+ trackEvent('message_delete', getMessageTrackingProps(row))
81
+ notifyParent('TEMPLATE_DELETED', { id: row.id, name: row.name })
82
+ useToast().add({
83
+ title: t('common.success'),
84
+ description: t('dialogs.delete_message_group.success', { name: row.name }),
85
+ color: 'success',
86
+ })
87
+ execute()
88
+ },
89
+ })
90
+ }
91
+
92
+ function askPublishGroup(row: MessageGroupWithMessageSummary) {
93
+ confirm({
94
+ title: t('dialogs.publish_message_group.title'),
95
+ message: t('dialogs.publish_message_group.message', { name: row.name }),
96
+ confirmLabel: t('common.actions.publish'),
97
+ cancelLabel: t('common.actions.cancel'),
98
+ action: async () => {
99
+ await publishGroup(row.id)
100
+ notifyParent('TEMPLATE_PUBLISHED', { id: row.id, name: row.name })
101
+ useToast().add({
102
+ title: t('common.success'),
103
+ description: t('dialogs.publish_message_group.success', { name: row.name }),
104
+ color: 'success',
105
+ })
106
+ execute()
107
+ },
108
+ })
109
+ }
110
+
111
+ function askUnpublishGroup(row: MessageGroupWithMessageSummary) {
112
+ confirm({
113
+ title: t('dialogs.unpublish_message_group.title'),
114
+ message: t('dialogs.unpublish_message_group.message', { name: row.name }),
115
+ confirmLabel: t('common.actions.unpublish'),
116
+ cancelLabel: t('common.actions.cancel'),
117
+ action: async () => {
118
+ await unpublishGroup(row.id)
119
+ notifyParent('TEMPLATE_UNPUBLISHED', { id: row.id, name: row.name })
120
+ useToast().add({
121
+ title: t('common.success'),
122
+ description: t('dialogs.unpublish_message_group.success', { name: row.name }),
123
+ color: 'success',
124
+ })
125
+ execute()
126
+ },
127
+ })
128
+ }
129
+
130
+ const { duplicateTemplate } = useTemplateOperations({
131
+ onSuccess: () => execute(), // Refresh the list
132
+ })
133
+
134
+ async function handleDuplicate(row: MessageGroupWithMessageSummary) {
135
+ trackEvent('message_duplicate', getMessageTrackingProps(row))
136
+ const result = await duplicateTemplate(row.id.toString(), t('common.copy_of', { name: row.name }))
137
+ if (result) {
138
+ notifyParent('TEMPLATE_DUPLICATED', { id: result.id, sourceId: row.id, name: result.name })
139
+ }
140
+ }
141
+
142
+ const router = useRouter()
143
+ function navigate(row: MessageGroupWithMessageSummary) {
144
+ trackEvent('message_edit', getMessageTrackingProps(row))
145
+ const type = row.messages[0]?.render_type === 'email_v1' ? 'email' : 'whatsapp'
146
+ router.push(`/template/${type}/${row.id}`)
147
+ }
148
+
149
+ const { t } = useI18n()
150
+
151
+ const tabItems = computed(() => {
152
+ const items = []
153
+ if (hasWhatsapp.value) {
154
+ items.push({
155
+ label: t('message.render_type.whatsapp_v1'),
156
+ value: 'whatsapp_v1',
157
+ slot: 'whatsapp_v1',
158
+ })
159
+ }
160
+ if (hasEmail.value) {
161
+ items.push({
162
+ label: t('message.render_type.email_v1'),
163
+ value: 'email_v1',
164
+ slot: 'email_v1',
165
+ })
166
+ }
167
+ return items
168
+ })
169
+ </script>
170
+
171
+ <template>
172
+ <SNavigationPage>
173
+ <template #header>
174
+ <SNavigationBarHeader
175
+ :title="$t('pages.message_library.title')"
176
+ :tabs="tabItems.length > 1 ? tabItems : undefined"
177
+ :active-tab="renderType"
178
+ :data-testid="homeTestIds.navbar"
179
+ @tab-change="renderType = $event"
180
+ >
181
+ <template #actions>
182
+ <CreateTemplateButton />
183
+ </template>
184
+ </SNavigationBarHeader>
185
+ </template>
186
+
187
+ <BaseTable
188
+ :loading="isFetching"
189
+ :message-groups="messageGroups || []"
190
+ :render-type="renderType"
191
+ @edit="navigate"
192
+ @delete="askDeleteGroup"
193
+ @duplicate="handleDuplicate"
194
+ @publish="askPublishGroup"
195
+ @unpublish="askUnpublishGroup"
196
+ />
197
+ </SNavigationPage>
198
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import WhatsappPreview from '@/components/Whatsapp/Preview.vue'
3
+ import EmailPreview from '@/components/Email/Preview.vue'
4
+ import { useRoute } from 'vue-router'
5
+ import { useI18n } from 'vue-i18n'
6
+ import { useMessageGroupQuery } from '~/queries/messageGroups'
7
+
8
+ const route = useRoute()
9
+ const { locale } = useI18n()
10
+ const { data: messageGroup } = useMessageGroupQuery(() => route.params.id as string)
11
+
12
+ const selectedMessage = computed(() => {
13
+ if (!messageGroup.value?.messages.length) return undefined
14
+
15
+ // 1. Try query param language
16
+ const langParam = route.query.lang as string | undefined
17
+ if (langParam) {
18
+ const msgByParam = messageGroup.value.messages.find((m) => m.language_id === langParam)
19
+ if (msgByParam) return msgByParam
20
+ }
21
+
22
+ // 2. Fallback to current locale
23
+ const msgByLocale = messageGroup.value.messages.find((m) => m.language_id === locale.value)
24
+ if (msgByLocale) return msgByLocale
25
+
26
+ // 3. Fallback to first message
27
+ return messageGroup.value.messages[0]
28
+ })
29
+
30
+ const brandId = computed(() => route.query.brand_id as string | undefined)
31
+ const isWhatsapp = computed(() => selectedMessage.value?.render_type === 'whatsapp_v1')
32
+ const isEmail = computed(() => selectedMessage.value?.render_type === 'email_v1')
33
+ </script>
34
+
35
+ <template>
36
+ <div :data-testid="previewPageTestIds.page" class="w-full overflow-y-auto">
37
+ <WhatsappPreview v-if="isWhatsapp && selectedMessage" :message="selectedMessage" />
38
+
39
+ <EmailPreview
40
+ v-else-if="isEmail && selectedMessage"
41
+ :message="selectedMessage"
42
+ :show-subject="route.query.showSubject === 'true'"
43
+ :message-group-id="route.params.id as string"
44
+ :brand-id="brandId"
45
+ />
46
+ </div>
47
+ </template>