@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,166 @@
1
+ <script lang="ts" setup>
2
+ import type {
3
+ WhatsappMessage,
4
+ WaButtonV1,
5
+ HeaderBlockV1,
6
+ } from '@dev.smartpricing/message-composer-utils/types'
7
+ import { toRaw } from 'vue'
8
+ import WhatsappEditor from './Index.vue'
9
+
10
+ interface Props {
11
+ /**
12
+ * All messages by language ID
13
+ */
14
+ messages: Record<string, WhatsappMessage>
15
+ /**
16
+ * Main language ID to edit
17
+ */
18
+ mainLanguageId: string
19
+ }
20
+
21
+ const props = defineProps<Props>()
22
+
23
+ const model = defineModel<WhatsappMessage>({ required: true })
24
+
25
+ /**
26
+ * Syncs button addition to all other languages
27
+ */
28
+ function handleButtonAdded(button: WaButtonV1) {
29
+ for (const langId in props.messages) {
30
+ if (langId === props.mainLanguageId) continue
31
+
32
+ const targetMsg = props.messages[langId]
33
+ if (!targetMsg?.body?.buttons) continue
34
+
35
+ // Check if button already exists
36
+ const exists = targetMsg.body.buttons.buttons.some((btn) => btn.id === button.id)
37
+ if (exists) continue
38
+
39
+ // Add button with same structure but empty text for translation
40
+ const newButton: WaButtonV1 = {
41
+ ...structuredClone(toRaw(button)),
42
+ text: '', // Empty for translation
43
+ }
44
+
45
+ targetMsg.body.buttons.buttons.push(newButton)
46
+ targetMsg.body.buttons.enabled = targetMsg.body.buttons.buttons.length > 0
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Syncs button removal to all other languages
52
+ */
53
+ function handleButtonRemoved(buttonId: string) {
54
+ for (const langId in props.messages) {
55
+ if (langId === props.mainLanguageId) continue
56
+
57
+ const targetMsg = props.messages[langId]
58
+ if (!targetMsg?.body?.buttons) continue
59
+
60
+ // Remove button by ID
61
+ targetMsg.body.buttons.buttons = targetMsg.body.buttons.buttons.filter(
62
+ (btn) => btn.id !== buttonId,
63
+ )
64
+ targetMsg.body.buttons.enabled = targetMsg.body.buttons.buttons.length > 0
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Syncs button type change to all other languages
70
+ */
71
+ function handleButtonTypeChanged(button: WaButtonV1) {
72
+ for (const langId in props.messages) {
73
+ if (langId === props.mainLanguageId) continue
74
+
75
+ const targetMsg = props.messages[langId]
76
+ if (!targetMsg?.body?.buttons) continue
77
+
78
+ // Find matching button by ID and update type/structure
79
+ const index = targetMsg.body.buttons.buttons.findIndex((btn) => btn.id === button.id)
80
+ if (index === -1) continue
81
+
82
+ const existingButton = targetMsg.body.buttons.buttons[index]
83
+ if (!existingButton) continue
84
+
85
+ // Update button structure while preserving translated text
86
+ const updatedButton: WaButtonV1 = {
87
+ ...structuredClone(toRaw(button)),
88
+ text: existingButton.text, // Keep translated text
89
+ }
90
+
91
+ targetMsg.body.buttons.buttons.splice(index, 1, updatedButton)
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Syncs header type change to all other languages.
97
+ * Always overrides target header with correct structure for the new type.
98
+ */
99
+ function handleHeaderTypeChanged(header: HeaderBlockV1) {
100
+ for (const langId in props.messages) {
101
+ if (langId === props.mainLanguageId) continue
102
+
103
+ const targetMsg = props.messages[langId]
104
+ if (!targetMsg?.body) continue
105
+
106
+ if (header.type === 'none') {
107
+ targetMsg.body.header = { type: 'none' }
108
+ } else if (header.type === 'text') {
109
+ targetMsg.body.header = { type: 'text', content: undefined }
110
+ } else {
111
+ // Media types: copy url and smartchatMediaId from base
112
+ targetMsg.body.header = structuredClone(toRaw(header))
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Syncs header media (url/metaMediaId) to all other languages.
119
+ * Always overrides all languages with the new media.
120
+ */
121
+ function handleHeaderMediaChanged(payload: { header: HeaderBlockV1; previousUrl: string }) {
122
+ if (payload.header.type === 'none' || payload.header.type === 'text') return
123
+
124
+ for (const langId in props.messages) {
125
+ if (langId === props.mainLanguageId) continue
126
+
127
+ const targetMsg = props.messages[langId]
128
+ if (!targetMsg?.body?.header) continue
129
+ if (targetMsg.body.header.type !== payload.header.type) continue
130
+
131
+ targetMsg.body.header = structuredClone(toRaw(payload.header))
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Syncs footer enabled state to all other languages
137
+ */
138
+ function handleFooterEnabledChanged(enabled: boolean) {
139
+ for (const langId in props.messages) {
140
+ if (langId === props.mainLanguageId) continue
141
+
142
+ const targetMsg = props.messages[langId]
143
+ if (!targetMsg?.body?.footer) continue
144
+
145
+ targetMsg.body.footer.enabled = enabled
146
+
147
+ // Clear content when disabled
148
+ if (!enabled) {
149
+ targetMsg.body.footer.content = ''
150
+ }
151
+ }
152
+ }
153
+ </script>
154
+
155
+ <template>
156
+ <WhatsappEditor
157
+ v-model="model"
158
+ @button-added="handleButtonAdded"
159
+ @button-removed="handleButtonRemoved"
160
+ @button-type-changed="handleButtonTypeChanged"
161
+ @header-type-changed="handleHeaderTypeChanged"
162
+ @header-media-changed="handleHeaderMediaChanged"
163
+ @footer-enabled-changed="handleFooterEnabledChanged"
164
+ :data-testid="whatsappEditorSyncWrapperTestIds.container"
165
+ />
166
+ </template>
@@ -0,0 +1,197 @@
1
+ <script lang="ts" setup>
2
+ import type {
3
+ PostEntity,
4
+ WaButtonTypeV1,
5
+ WhatsappMessage,
6
+ } from '@dev.smartpricing/message-composer-utils/types'
7
+ import { generateHTML } from '@tiptap/vue-3'
8
+ import { computed } from 'vue'
9
+ import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
10
+
11
+ defineOptions({
12
+ name: 'WhatsappPreview',
13
+ })
14
+
15
+ const props = defineProps<{
16
+ message: PostEntity<WhatsappMessage>
17
+ dynamicData?: Record<string, any>
18
+ }>()
19
+
20
+ function replaceMentionsWithValues(html: string): string {
21
+ if (!props.dynamicData) return html
22
+
23
+ let replacedHtml = html
24
+ for (const [key, value] of Object.entries(props.dynamicData)) {
25
+ const mentionRegex = new RegExp(`{{${key}}}`, 'g')
26
+ replacedHtml = replacedHtml.replace(mentionRegex, value || `{{${key}}}`)
27
+ }
28
+
29
+ return replacedHtml
30
+ }
31
+
32
+ const bodyHtml = computed(() => {
33
+ if (!props.message.body.body.content) return ''
34
+ const html = generateHTML(
35
+ props.message.body.body.content,
36
+ getTipTapExtensions({ preset: 'whatsapp' }),
37
+ ).replaceAll('<p></p>', '<p>&nbsp;</p>')
38
+ return replaceMentionsWithValues(html)
39
+ })
40
+
41
+ const headerHtml = computed(() => {
42
+ if (props.message.body.header.type !== 'text') return ''
43
+ if (!props.message.body.header.content) return ''
44
+ const html = generateHTML(
45
+ props.message.body.header.content,
46
+ getTipTapExtensions({ preset: 'whatsapp-header' }),
47
+ ).replaceAll('<p></p>', '<p>&nbsp;</p>')
48
+ return replaceMentionsWithValues(html)
49
+ })
50
+
51
+ const currentTime = computed(() => {
52
+ const now = new Date()
53
+ return now.toLocaleTimeString('en-US', {
54
+ hour: '2-digit',
55
+ minute: '2-digit',
56
+ hour12: true,
57
+ })
58
+ })
59
+
60
+ const hasHeader = computed(() => props.message.body.header.type !== 'none')
61
+ const hasFooter = computed(
62
+ () => props.message.body.footer.enabled && props.message.body.footer.content,
63
+ )
64
+ const hasButtons = computed(
65
+ () => props.message.body.buttons.enabled && props.message.body.buttons.buttons.length > 0,
66
+ )
67
+
68
+ const getIcon = (type: WaButtonTypeV1) => {
69
+ if (type === 'unsubscribe') return 'ph:arrow-line-up-right'
70
+ if (type === 'link') return 'ph:arrow-line-up-right'
71
+ if (type === 'phone') return 'ph:phone-fill'
72
+ return null
73
+ }
74
+ </script>
75
+
76
+ <template>
77
+ <div
78
+ class="w-full flex flex-col items-center justify-start p-4"
79
+ :data-testid="whatsappPreviewTestIds.container"
80
+ >
81
+ <div
82
+ class="bg-[#f3edea] rounded border-4 border-transparent p-4 w-full max-w-[306px] min-h-[400px] relative"
83
+ :data-testid="whatsappPreviewTestIds.chatWindow"
84
+ >
85
+ <!-- Chat Bubble Container -->
86
+ <div class="flex flex-col items-end w-full">
87
+ <!-- Message Bubble -->
88
+ <div class="flex items-end justify-start w-full shadow-sm">
89
+ <!-- Speech Bubble Tail -->
90
+ <div class="flex items-center justify-center flex-shrink-0">
91
+ <div class="w-2 h-4 transform rotate-180 scale-y-[-1]">
92
+ <svg
93
+ viewBox="0 0 8 15"
94
+ fill="none"
95
+ xmlns="http://www.w3.org/2000/svg"
96
+ class="w-full h-full"
97
+ >
98
+ <path d="M0 0v15c0-8.284 6.716-15 15-15H0z" fill="white" />
99
+ </svg>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Message Content -->
104
+ <div class="bg-white rounded-md p-1 flex-1 min-w-0">
105
+ <div class="flex flex-col w-full">
106
+ <!-- Message Content Section -->
107
+ <div class="flex flex-col p-1.5 pb-0">
108
+ <!-- Header Content (if exists) -->
109
+ <div v-if="hasHeader" class="mb-2" :data-testid="whatsappPreviewTestIds.header">
110
+ <div
111
+ v-if="message.body.header.type === 'text'"
112
+ class="text-sm text-primary-950 break-all"
113
+ >
114
+ <p v-html="headerHtml" />
115
+ </div>
116
+ <div v-else-if="message.body.header.type === 'image'" class="mb-2">
117
+ <img
118
+ v-show="message.body.header.url"
119
+ :src="message.body.header.url"
120
+ :alt="$t('editor.whatsapp.preview.header_image_alt')"
121
+ class="max-w-full h-auto rounded"
122
+ />
123
+ </div>
124
+ <div v-else-if="message.body.header.type === 'video'" class="mb-2">
125
+ <video
126
+ v-show="message.body.header.url"
127
+ :src="message.body.header.url"
128
+ controls
129
+ class="max-w-full h-auto rounded"
130
+ />
131
+ </div>
132
+ <div v-else-if="message.body.header.type === 'document'" class="mb-2">
133
+ <a
134
+ v-show="message.body.header.url"
135
+ :href="message.body.header.url"
136
+ class="text-blue-600 underline text-sm"
137
+ >
138
+ 📎 {{ $t('editor.whatsapp.preview.document_link') }}
139
+ </a>
140
+ </div>
141
+ </div>
142
+
143
+ <!-- Body Content -->
144
+ <div
145
+ class="text-sm text-primary-950 leading-[18px] break-all"
146
+ v-html="bodyHtml"
147
+ :data-testid="whatsappPreviewTestIds.body"
148
+ />
149
+
150
+ <!-- Footer Content -->
151
+ <div
152
+ v-if="hasFooter"
153
+ class="mt-1 pt-0.5"
154
+ :data-testid="whatsappPreviewTestIds.footer"
155
+ >
156
+ <div class="text-sm text-primary-600 leading-[18px]">
157
+ {{ message.body.footer.content }}
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Timestamp -->
163
+ <div class="px-1.5 pb-1 pt-2">
164
+ <div class="text-xs text-primary-600 text-right leading-3">
165
+ {{ currentTime }}
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ <div
173
+ v-if="hasButtons"
174
+ class="mt-2 ml-2"
175
+ :data-testid="whatsappPreviewTestIds.buttonsContainer"
176
+ >
177
+ <div class="flex flex-col gap-2">
178
+ <div
179
+ v-for="button in message.body.buttons.buttons"
180
+ :key="button.id"
181
+ class="p-2 bg-white text-[#0096DE] flex items-center justify-center rounded-md text-sm gap-1"
182
+ :data-testid="`${whatsappPreviewTestIds.button}-${button.id}`"
183
+ >
184
+ <UIcon
185
+ v-if="getIcon(button.type)"
186
+ :name="getIcon(button.type) ?? ''"
187
+ :size="16"
188
+ class="fill-current"
189
+ />
190
+
191
+ {{ button.text }}
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </template>
@@ -0,0 +1,24 @@
1
+ <script lang="ts" setup>
2
+ import type { PostEntity, WhatsappMessage } from '@dev.smartpricing/message-composer-utils/types'
3
+ import WhatsappPreview from './Preview.vue'
4
+
5
+ defineOptions({
6
+ name: 'WhatsappPreviewPanel',
7
+ })
8
+
9
+ defineProps<{
10
+ message: PostEntity<WhatsappMessage>
11
+ dynamicData?: Record<string, any>
12
+ }>()
13
+ </script>
14
+
15
+ <template>
16
+ <div class="bg-white flex flex-col w-full" :data-testid="whatsappPreviewPanelTestIds.container">
17
+ <!-- Header Section -->
18
+ <div class="flex items-center justify-between p-4 pb-0">
19
+ <h3 class="label-lg">{{ $t('common.preview') }}</h3>
20
+ </div>
21
+
22
+ <WhatsappPreview :message="message" :dynamic-data="dynamicData" />
23
+ </div>
24
+ </template>
@@ -0,0 +1,150 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue'
3
+ import type { WhatsappMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
4
+ import WhatsappTranslator from '@/components/Whatsapp/Translator/Index.vue'
5
+ import TranslationActionBar from '@/components/Common/TranslationActionBar.vue'
6
+
7
+ interface Props {
8
+ baseMessage: WhatsappMessage | PostEntity<WhatsappMessage>
9
+ messages: Record<string, PostEntity<WhatsappMessage>>
10
+ currentLanguage: string
11
+ baseLanguage: string
12
+ messageGroupId?: string | number
13
+ }
14
+
15
+ const props = defineProps<Props>()
16
+
17
+ const emit = defineEmits<{
18
+ 'auto-translate-tracked': [language: string]
19
+ preview: [language: string]
20
+ 'filters-changed': [
21
+ payload: {
22
+ language: string
23
+ search_term: string
24
+ show_only_missing: boolean
25
+ show_only_changes: boolean
26
+ },
27
+ ]
28
+ }>()
29
+
30
+ const targetMessage = defineModel<WhatsappMessage | PostEntity<WhatsappMessage>>('targetMessage', {
31
+ required: true,
32
+ })
33
+
34
+ // Filter states
35
+ const searchQuery = ref('')
36
+ const showOnlyMissing = ref(false)
37
+ const showOnlyChanges = ref(false)
38
+
39
+ // Auto-translate composable
40
+ const { translateMessage, isTranslating, error } = useAutoTranslate()
41
+
42
+ // Filters composable
43
+ const messagesRef = computed(() => props.messages)
44
+ const currentLanguageRef = computed(() => props.currentLanguage)
45
+ const baseLanguageRef = computed(() => props.baseLanguage)
46
+ const messageGroupIdRef = computed(() => props.messageGroupId)
47
+
48
+ const { filteredSections, hasActiveFilters } = useTranslationFilters({
49
+ messages: messagesRef,
50
+ currentLanguage: currentLanguageRef,
51
+ baseLanguage: baseLanguageRef,
52
+ messageGroupId: messageGroupIdRef,
53
+ searchQuery,
54
+ showOnlyMissing,
55
+ showOnlyChanges,
56
+ })
57
+
58
+ // Event handlers
59
+ async function handleAutoTranslate() {
60
+ const baseMsg = props.messages[props.baseLanguage]
61
+ const targetMsg = props.messages[props.currentLanguage]
62
+
63
+ if (!baseMsg || !targetMsg) {
64
+ return
65
+ }
66
+
67
+ emit('auto-translate-tracked', props.currentLanguage)
68
+
69
+ const success = await translateMessage(
70
+ baseMsg,
71
+ targetMsg,
72
+ props.baseLanguage,
73
+ props.currentLanguage,
74
+ )
75
+
76
+ if (success) {
77
+ // Success notification will be handled by parent or toast
78
+ console.log('Translation completed successfully')
79
+ } else if (error.value) {
80
+ // Error notification will be handled by parent or toast
81
+ console.error('Translation failed:', error.value)
82
+ }
83
+ }
84
+
85
+ function handlePreview() {
86
+ emit('preview', props.currentLanguage)
87
+ }
88
+
89
+ // Emit filters-changed when any filter changes
90
+ watch([searchQuery, showOnlyMissing, showOnlyChanges], () => {
91
+ emit('filters-changed', {
92
+ language: props.currentLanguage,
93
+ search_term: searchQuery.value,
94
+ show_only_missing: showOnlyMissing.value,
95
+ show_only_changes: showOnlyChanges.value,
96
+ })
97
+ })
98
+
99
+ function clearFilters() {
100
+ searchQuery.value = ''
101
+ showOnlyMissing.value = false
102
+ showOnlyChanges.value = false
103
+ }
104
+ </script>
105
+
106
+ <template>
107
+ <div class="flex flex-col relative" :data-testid="whatsappTranslationEditorTestIds.container">
108
+ <!-- Action Bar -->
109
+ <TranslationActionBar
110
+ :messages="messages"
111
+ :current-language="currentLanguage"
112
+ :base-language="baseLanguage"
113
+ :message-group-id="messageGroupId"
114
+ :is-translating="isTranslating"
115
+ v-model:search-query="searchQuery"
116
+ v-model:show-only-missing="showOnlyMissing"
117
+ v-model:show-only-changes="showOnlyChanges"
118
+ @auto-translate="handleAutoTranslate"
119
+ @preview="handlePreview"
120
+ />
121
+
122
+ <!-- Empty State when no sections match filters -->
123
+ <UEmpty
124
+ v-if="hasActiveFilters && filteredSections.length === 0"
125
+ icon="ph:funnel"
126
+ :title="$t('translation_action_bar.no_sections_found')"
127
+ :data-testid="whatsappTranslationEditorTestIds.emptyState"
128
+ >
129
+ <template #actions>
130
+ <UButton
131
+ color="neutral"
132
+ variant="outline"
133
+ size="sm"
134
+ @click="clearFilters"
135
+ :data-testid="whatsappTranslationEditorTestIds.clearFiltersButton"
136
+ >
137
+ {{ $t('translation_action_bar.clear_filters') }}
138
+ </UButton>
139
+ </template>
140
+ </UEmpty>
141
+
142
+ <!-- Translator -->
143
+ <WhatsappTranslator
144
+ v-if="!hasActiveFilters || filteredSections.length > 0"
145
+ :base-message="baseMessage"
146
+ v-model:target-message="targetMessage"
147
+ :filtered-sections="hasActiveFilters ? filteredSections : undefined"
148
+ />
149
+ </div>
150
+ </template>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import type { JSONContent } from '@tiptap/vue-3'
3
+ import { generateHTML } from '@tiptap/vue-3'
4
+ import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
5
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
6
+ import { useAppContextStore } from '@/stores/appContext'
7
+ import { WA_BODY_MAX_LENGTH } from '@dev.smartpricing/message-composer-utils/types'
8
+
9
+ defineOptions({
10
+ name: 'WhatsappTranslatorBody',
11
+ })
12
+
13
+ const { baseContent } = defineProps<{
14
+ baseContent: JSONContent | undefined
15
+ }>()
16
+
17
+ const targetContent = defineModel<JSONContent | undefined>({
18
+ required: true,
19
+ })
20
+
21
+ const appContext = useAppContextStore()
22
+
23
+ // Generate HTML for preview of base language
24
+ function generatePreviewHtml(content: JSONContent | undefined): string {
25
+ if (!content) return ''
26
+ return generateHTML(content, getTipTapExtensions({ preset: 'whatsapp' })).replaceAll(
27
+ '<p></p>',
28
+ '<p>&nbsp;</p>',
29
+ )
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div class="flex gap-8 items-start" :data-testid="whatsappTranslatorBodyTestIds.container">
35
+ <!-- Base language preview (read-only) -->
36
+ <div class="flex-1 min-w-0" :data-testid="whatsappTranslatorBodyTestIds.basePreview">
37
+ <div
38
+ class="paragraph-sm text-primary-700 break-words"
39
+ v-html="generatePreviewHtml(baseContent)"
40
+ />
41
+ </div>
42
+
43
+ <!-- Translation editor (editable) -->
44
+ <div class="flex-1 min-w-0 flex gap-2 items-start">
45
+ <div class="flex-1">
46
+ <RichTextEditor
47
+ v-model:text="targetContent"
48
+ preset="whatsapp"
49
+ dynamic-values
50
+ bold
51
+ italic
52
+ emoji
53
+ :character-limit="WA_BODY_MAX_LENGTH"
54
+ :data-testid="whatsappTranslatorBodyTestIds.targetEditor"
55
+ />
56
+ </div>
57
+
58
+ <slot name="actions" />
59
+ </div>
60
+ </div>
61
+ </template>