@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,108 @@
1
+ <script lang="ts" setup>
2
+ import type { DividerBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import {
4
+ DIVIDER_MIN_THICKNESS,
5
+ DIVIDER_MAX_THICKNESS,
6
+ DIVIDER_MIN_WIDTH,
7
+ DIVIDER_MAX_WIDTH,
8
+ DEFAULT_DIVIDER_SETTINGS,
9
+ } from '@dev.smartpricing/message-composer-utils/types'
10
+ import ColorInput from '~/components/Common/ColorInput.vue'
11
+
12
+ const props = defineProps<{
13
+ block: DividerBlock
14
+ }>()
15
+
16
+ const emailEditorStore = useEmailEditorStore()
17
+
18
+ const styleOptions = [
19
+ { value: 'solid', label: 'Solid' },
20
+ { value: 'dashed', label: 'Dashed' },
21
+ { value: 'dotted', label: 'Dotted' },
22
+ ]
23
+
24
+ function resetAll() {
25
+ emailEditorStore.updateBlock(props.block.id, {
26
+ settings: { ...DEFAULT_DIVIDER_SETTINGS },
27
+ })
28
+ }
29
+
30
+ defineOptions({
31
+ name: 'DividerSettings',
32
+ })
33
+ </script>
34
+
35
+ <template>
36
+ <div class="space-y-6 p-4" :data-testid="dividerSettingsTestIds.container">
37
+ <!-- Style -->
38
+ <div class="space-y-3">
39
+ <p class="paragraph-md-bold">
40
+ {{ $t('editor.email.blocks.divider.settings.style') }}
41
+ </p>
42
+ <UTabs
43
+ v-model="block.settings.style"
44
+ :items="styleOptions"
45
+ :data-testid="dividerSettingsTestIds.styleTabs"
46
+ />
47
+
48
+ <!-- Thickness -->
49
+ <UFormField
50
+ :label="$t('editor.email.blocks.divider.settings.thickness')"
51
+ :data-testid="dividerSettingsTestIds.thicknessField"
52
+ >
53
+ <USlider
54
+ v-model="block.settings.thickness"
55
+ :min="DIVIDER_MIN_THICKNESS"
56
+ :max="DIVIDER_MAX_THICKNESS"
57
+ :data-testid="dividerSettingsTestIds.thicknessSlider"
58
+ />
59
+ </UFormField>
60
+
61
+ <!-- Width -->
62
+ <UFormField
63
+ :label="$t('editor.email.blocks.divider.settings.width')"
64
+ :hint="$t('editor.email.blocks.divider.settings.widthHint')"
65
+ :data-testid="dividerSettingsTestIds.widthField"
66
+ >
67
+ <UFieldGroup class="w-full">
68
+ <UInputNumber
69
+ v-model="block.settings.width"
70
+ :min="DIVIDER_MIN_WIDTH"
71
+ :max="DIVIDER_MAX_WIDTH"
72
+ :data-testid="dividerSettingsTestIds.widthInput"
73
+ />
74
+ <UBadge color="neutral" variant="outline">%</UBadge>
75
+ </UFieldGroup>
76
+ </UFormField>
77
+ </div>
78
+
79
+ <!-- Color -->
80
+ <div class="space-y-3">
81
+ <p class="paragraph-md-bold">
82
+ {{ $t('editor.email.blocks.divider.settings.color') }}
83
+ </p>
84
+ <ColorInput
85
+ v-model="block.settings.color"
86
+ :label="$t('editor.email.blocks.divider.settings.color')"
87
+ />
88
+
89
+ <!-- Background Color -->
90
+ <ColorInput
91
+ v-model="block.settings.backgroundColor"
92
+ :label="$t('editor.email.blocks.divider.settings.backgroundColor')"
93
+ />
94
+ </div>
95
+
96
+ <!-- Reset All Button -->
97
+ <UButton
98
+ variant="outline"
99
+ color="neutral"
100
+ block
101
+ icon="ph:arrow-counter-clockwise"
102
+ :data-testid="dividerSettingsTestIds.resetButton"
103
+ @click="resetAll"
104
+ >
105
+ {{ $t('editor.email.common.reset') }}
106
+ </UButton>
107
+ </div>
108
+ </template>
@@ -0,0 +1,61 @@
1
+ <script lang="ts" setup>
2
+ import type { FooterBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { useEditor, EditorContent } from '@tiptap/vue-3'
4
+ import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
5
+ import { useAppContextStore } from '@/stores/appContext'
6
+
7
+ const props = defineProps<{
8
+ block: FooterBlock
9
+ }>()
10
+
11
+ const appContext = useAppContextStore()
12
+ const emailEditorStore = useEmailEditorStore()
13
+
14
+ // Read-only editor for preview
15
+ const editor = useEditor({
16
+ content: props.block.settings.content,
17
+ editable: false,
18
+ extensions: getTipTapExtensions({
19
+ preset: 'email',
20
+ dynamicValues: true,
21
+ }),
22
+ })
23
+
24
+ // Update preview when content changes
25
+ watch(
26
+ () => props.block.settings.content,
27
+ (newContent) => {
28
+ if (editor.value && newContent) {
29
+ editor.value.commands.setContent(newContent, { emitUpdate: false })
30
+ }
31
+ },
32
+ )
33
+
34
+ defineOptions({
35
+ name: 'FooterBlock',
36
+ })
37
+ </script>
38
+
39
+ <template>
40
+ <div
41
+ class="rounded p-2 relative"
42
+ :style="{ backgroundColor: block.settings.backgroundColor }"
43
+ :data-testid="footerBlockTestIds.container"
44
+ >
45
+ <div
46
+ :style="{
47
+ fontFamily: block.settings.fontFamily ?? emailEditorStore.resolvedSettings.fontFamily,
48
+ fontSize: `${block.settings.fontSize}px`,
49
+ color: block.settings.textColor ?? emailEditorStore.resolvedSettings.textColor,
50
+ }"
51
+ >
52
+ <EditorContent :editor="editor" />
53
+ <div
54
+ v-show="editor?.isEmpty"
55
+ class="absolute inset-0 w-full h-full flex items-center justify-center text-neutral-400 text-sm"
56
+ >
57
+ {{ $t('editor.placeholder.insert_text') }}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </template>
@@ -0,0 +1,123 @@
1
+ <script lang="ts" setup>
2
+ import type { FooterBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { DEFAULT_FOOTER_SETTINGS } from '@dev.smartpricing/message-composer-utils/types'
4
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
5
+ import { useAppContextStore } from '@/stores/appContext'
6
+ import ColorInput from '~/components/Common/ColorInput.vue'
7
+ import FontFamilySelect from '~/components/Common/FontFamilySelect.vue'
8
+ import SectionHeader from '~/components/Common/SectionHeader.vue'
9
+
10
+ const props = defineProps<{
11
+ block: FooterBlock
12
+ }>()
13
+
14
+ const emailEditorStore = useEmailEditorStore()
15
+ const appContext = useAppContextStore()
16
+
17
+ const content = computed({
18
+ get: () => props.block.settings.content,
19
+ set: (value) => {
20
+ emailEditorStore.updateBlock(props.block.id, {
21
+ settings: { ...props.block.settings, content: value },
22
+ })
23
+ },
24
+ })
25
+
26
+ function resetTextStyle() {
27
+ emailEditorStore.updateBlock(props.block.id, {
28
+ settings: {
29
+ ...props.block.settings,
30
+ fontFamily: DEFAULT_FOOTER_SETTINGS.fontFamily,
31
+ fontSize: DEFAULT_FOOTER_SETTINGS.fontSize,
32
+ },
33
+ })
34
+ }
35
+
36
+ function resetColors() {
37
+ emailEditorStore.updateBlock(props.block.id, {
38
+ settings: {
39
+ ...props.block.settings,
40
+ textColor: DEFAULT_FOOTER_SETTINGS.textColor,
41
+ backgroundColor: DEFAULT_FOOTER_SETTINGS.backgroundColor,
42
+ },
43
+ })
44
+ }
45
+
46
+ defineOptions({
47
+ name: 'FooterSettings',
48
+ })
49
+ </script>
50
+
51
+ <template>
52
+ <div class="space-y-6 p-4" :data-testid="footerSettingsTestIds.container">
53
+ <!-- Content Editor -->
54
+ <div class="space-y-3">
55
+ <p class="paragraph-md-bold">
56
+ {{ $t('editor.email.blocks.heading.settings.content') }}
57
+ </p>
58
+ <RichTextEditor
59
+ v-model:text="content"
60
+ preset="email"
61
+ bold
62
+ italic
63
+ underline
64
+ alignment
65
+ link
66
+ emoji
67
+ dynamic-values
68
+ />
69
+ </div>
70
+
71
+ <!-- Text Style Section -->
72
+ <div class="space-y-3">
73
+ <SectionHeader
74
+ :title="$t('editor.email.blocks.heading.settings.textStyle')"
75
+ show-reset
76
+ @reset="resetTextStyle"
77
+ />
78
+
79
+ <!-- Font Family & Size Row -->
80
+ <div class="grid grid-cols-2 gap-3">
81
+ <FontFamilySelect
82
+ v-model="block.settings.fontFamily"
83
+ :label="$t('editor.email.blocks.heading.settings.font')"
84
+ :default-value="emailEditorStore.resolvedSettings.fontFamily"
85
+ />
86
+ <UFormField
87
+ :label="$t('editor.email.blocks.heading.settings.size')"
88
+ :data-testid="footerSettingsTestIds.fontSizeField"
89
+ >
90
+ <UFieldGroup class="w-full">
91
+ <UInputNumber
92
+ v-model="block.settings.fontSize"
93
+ :data-testid="footerSettingsTestIds.fontSizeInput"
94
+ />
95
+ <UBadge color="neutral" variant="outline">px</UBadge>
96
+ </UFieldGroup>
97
+ </UFormField>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Color Section -->
102
+ <div class="space-y-3">
103
+ <SectionHeader
104
+ :title="$t('editor.email.blocks.heading.settings.color')"
105
+ show-reset
106
+ @reset="resetColors"
107
+ />
108
+
109
+ <!-- Text Color -->
110
+ <ColorInput
111
+ v-model="block.settings.textColor"
112
+ :label="$t('editor.email.blocks.heading.settings.text')"
113
+ :default-value="emailEditorStore.resolvedSettings.textColor"
114
+ />
115
+
116
+ <!-- Background Color -->
117
+ <ColorInput
118
+ v-model="block.settings.backgroundColor"
119
+ :label="$t('editor.email.blocks.heading.settings.backgroundBlock')"
120
+ />
121
+ </div>
122
+ </div>
123
+ </template>
@@ -0,0 +1,128 @@
1
+ <script lang="ts" setup>
2
+ import type { GridAllowedBlock, GridBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { emptyEmailBlock } from '@dev.smartpricing/message-composer-utils/types'
4
+ import IndexBlock from './IndexBlock.vue'
5
+
6
+ const props = defineProps<{
7
+ block: GridBlock
8
+ }>()
9
+
10
+ const emailEditorStore = useEmailEditorStore()
11
+
12
+ // Available block types for grid columns (no grid, header, footer, html, discount)
13
+ const allowedBlockTypes = [
14
+ 'heading',
15
+ 'paragraph',
16
+ 'image',
17
+ 'button',
18
+ 'discount',
19
+ ] satisfies GridAllowedBlock['type'][]
20
+
21
+ const { t } = useI18n()
22
+
23
+ function getDropdownItems(columnId: string) {
24
+ return [
25
+ allowedBlockTypes.map((blockType) => ({
26
+ label: t(`editor.email.blocks.${blockType}.name`),
27
+ onSelect: () => addBlockToColumn(columnId, blockType),
28
+ })),
29
+ ]
30
+ }
31
+
32
+ // Initialize columnData if empty
33
+ if (!props.block.settings.columnData || props.block.settings.columnData.length === 0) {
34
+ const columns = Array.from({ length: props.block.settings.columns }, () => ({
35
+ id: crypto.randomUUID(),
36
+ blocks: [],
37
+ }))
38
+ emailEditorStore.updateBlock(props.block.id, {
39
+ settings: { ...props.block.settings, columnData: columns },
40
+ })
41
+ }
42
+
43
+ function addBlockToColumn(columnId: string, blockType: GridAllowedBlock['type']) {
44
+ const newBlock = emptyEmailBlock(blockType) as GridAllowedBlock
45
+ const updatedColumns = props.block.settings.columnData.map((col) =>
46
+ col.id === columnId ? { ...col, blocks: [...col.blocks, newBlock] } : col,
47
+ )
48
+ emailEditorStore.updateBlock(props.block.id, {
49
+ settings: { ...props.block.settings, columnData: updatedColumns },
50
+ })
51
+ }
52
+
53
+ function handleBlockClick(blockId: string) {
54
+ emailEditorStore.selectBlock(blockId)
55
+ }
56
+
57
+ function handleDeleteBlock(blockId: string) {
58
+ // Find and remove block from its column
59
+ const updatedColumns = props.block.settings.columnData.map((col) => ({
60
+ ...col,
61
+ blocks: col.blocks.filter((b) => b.id !== blockId),
62
+ }))
63
+ emailEditorStore.updateBlock(props.block.id, {
64
+ settings: { ...props.block.settings, columnData: updatedColumns },
65
+ })
66
+ }
67
+
68
+ defineOptions({
69
+ name: 'GridBlock',
70
+ })
71
+ </script>
72
+
73
+ <template>
74
+ <div
75
+ class="rounded p-4"
76
+ :style="{ backgroundColor: block.settings.backgroundColor }"
77
+ :data-testid="gridBlockTestIds.container"
78
+ >
79
+ <div
80
+ class="grid gap-4"
81
+ :class="{
82
+ 'grid-cols-1': block.settings.columns === 1,
83
+ 'grid-cols-2': block.settings.columns === 2,
84
+ 'grid-cols-3': block.settings.columns === 3,
85
+ }"
86
+ >
87
+ <div
88
+ v-for="(column, index) in block.settings.columnData"
89
+ :key="column.id"
90
+ class="min-h-32 space-y-2 rounded border border-dashed border-neutral-300 p-2 overflow-hidden"
91
+ :data-testid="`${gridBlockTestIds.column}-${column.id}`"
92
+ >
93
+ <!-- Column blocks -->
94
+ <IndexBlock
95
+ v-for="columnBlock in column.blocks"
96
+ :key="columnBlock.id"
97
+ :block="columnBlock"
98
+ :is-selected="emailEditorStore.selectedBlockId === columnBlock.id"
99
+ :show-drag-handle="false"
100
+ :show-duplicate="false"
101
+ :show-label="false"
102
+ @click="handleBlockClick"
103
+ @delete="handleDeleteBlock"
104
+ />
105
+
106
+ <!-- Empty column placeholder -->
107
+ <div
108
+ v-if="column.blocks.length === 0"
109
+ class="flex h-full min-h-32 items-center justify-center"
110
+ >
111
+ <UDropdownMenu
112
+ :items="getDropdownItems(column.id)"
113
+ :data-testid="`${gridBlockTestIds.addElementDropdown}-${column.id}`"
114
+ >
115
+ <UButton
116
+ variant="ghost"
117
+ color="neutral"
118
+ icon="i-heroicons-plus"
119
+ :data-testid="`${gridBlockTestIds.addElementButton}-${column.id}`"
120
+ >
121
+ {{ $t('editor.email.blocks.grid.add_element') }}
122
+ </UButton>
123
+ </UDropdownMenu>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </template>
@@ -0,0 +1,109 @@
1
+ <script lang="ts" setup>
2
+ import type { GridBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import ColorInput from '~/components/Common/ColorInput.vue'
4
+
5
+ const props = defineProps<{
6
+ block: GridBlock
7
+ }>()
8
+
9
+ const emailEditorStore = useEmailEditorStore()
10
+ const { t } = useI18n()
11
+ const { confirm } = useConfirm()
12
+
13
+ const columnOptions = [
14
+ { value: '1', label: '1' },
15
+ { value: '2', label: '2' },
16
+ { value: '3', label: '3' },
17
+ ]
18
+
19
+ const selectedColumn = computed({
20
+ get: () => String(props.block.settings.columns),
21
+ set: (value) => {
22
+ updateColumns(Number(value) as 1 | 2 | 3)
23
+ },
24
+ })
25
+
26
+ function updateColumns(newColumns: 1 | 2 | 3) {
27
+ const currentColumns = props.block.settings.columnData.length
28
+
29
+ // If reducing columns, warn about data loss
30
+ if (newColumns < currentColumns) {
31
+ const hasContentInRemovedColumns = props.block.settings.columnData
32
+ .slice(newColumns)
33
+ .some((col) => col.blocks.length > 0)
34
+
35
+ if (hasContentInRemovedColumns) {
36
+ confirm({
37
+ title: t('dialogs.grid_reduce_columns.title'),
38
+ message: t('dialogs.grid_reduce_columns.message'),
39
+ confirmLabel: t('dialogs.grid_reduce_columns.confirm'),
40
+ cancelLabel: t('common.actions.cancel'),
41
+ action: () => applyColumnChange(newColumns),
42
+ })
43
+ return
44
+ }
45
+ }
46
+
47
+ applyColumnChange(newColumns)
48
+ }
49
+
50
+ function applyColumnChange(newColumns: 1 | 2 | 3) {
51
+ const currentColumns = props.block.settings.columnData.length
52
+
53
+ // Create new column data
54
+ const columnData = Array.from({ length: newColumns }, (_, i) => {
55
+ // Keep existing columns if available
56
+ if (i < currentColumns) {
57
+ return props.block.settings.columnData[i]
58
+ }
59
+ // Create new empty column
60
+ return {
61
+ id: crypto.randomUUID(),
62
+ blocks: [],
63
+ }
64
+ }) as GridBlock['settings']['columnData']
65
+
66
+ emailEditorStore.updateBlock(props.block.id, {
67
+ settings: {
68
+ ...props.block.settings,
69
+ columns: newColumns,
70
+ columnData,
71
+ },
72
+ })
73
+ }
74
+
75
+ defineOptions({
76
+ name: 'GridSettings',
77
+ })
78
+ </script>
79
+
80
+ <template>
81
+ <div class="space-y-6 p-4" :data-testid="gridSettingsTestIds.container">
82
+ <!-- Columns Section -->
83
+ <div class="space-y-3">
84
+ <p class="paragraph-md-bold">
85
+ {{ $t('editor.email.blocks.grid.settings.columns') }}
86
+ </p>
87
+ <UTabs
88
+ v-model="selectedColumn"
89
+ :items="columnOptions"
90
+ :content="false"
91
+ size="sm"
92
+ :data-testid="gridSettingsTestIds.columnsTabs"
93
+ />
94
+ </div>
95
+
96
+ <!-- Color Section -->
97
+ <div class="space-y-3">
98
+ <p class="paragraph-md-bold">
99
+ {{ $t('editor.email.blocks.grid.settings.color') }}
100
+ </p>
101
+
102
+ <!-- Background Color -->
103
+ <ColorInput
104
+ v-model="block.settings.backgroundColor"
105
+ :label="$t('editor.email.blocks.grid.settings.background_block')"
106
+ />
107
+ </div>
108
+ </div>
109
+ </template>
@@ -0,0 +1,31 @@
1
+ <script lang="ts" setup>
2
+ import type { RawHtmlBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import HtmlPreviewIframe from '@/components/Common/HtmlPreviewIframe.vue'
4
+
5
+ defineProps<{
6
+ block: RawHtmlBlock
7
+ }>()
8
+
9
+ defineOptions({
10
+ name: 'HTMLBlock',
11
+ })
12
+ </script>
13
+
14
+ <template>
15
+ <div class="relative" :data-testid="htmlBlockTestIds.container">
16
+ <HtmlPreviewIframe
17
+ :html="block.settings.html"
18
+ title="HTML block preview"
19
+ class="w-full block pointer-events-none"
20
+ :data-testid="htmlBlockTestIds.content"
21
+ v-if="block.settings.html?.trim()"
22
+ />
23
+ <div
24
+ v-else
25
+ class="flex items-center justify-center text-neutral-400 text-sm p-8"
26
+ :data-testid="htmlBlockTestIds.placeholder"
27
+ >
28
+ {{ $t('editor.email.blocks.raw_html.placeholder') }}
29
+ </div>
30
+ </div>
31
+ </template>
@@ -0,0 +1,85 @@
1
+ <script lang="ts" setup>
2
+ import type { RawHtmlBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { HTML_MAX_LENGTH } from '@dev.smartpricing/message-composer-utils/types'
4
+ import MonacoEditor from '@/components/Common/MonacoEditor.vue'
5
+ import { computed } from 'vue'
6
+ import { slideover } from '#build/ui'
7
+
8
+ const props = defineProps<{
9
+ block: RawHtmlBlock
10
+ }>()
11
+
12
+ const emailEditorStore = useEmailEditorStore()
13
+
14
+ const htmlContent = computed({
15
+ get: () => props.block.settings.html,
16
+ set: (value: string) => {
17
+ emailEditorStore.updateBlock(props.block.id, {
18
+ settings: { ...props.block.settings, html: value },
19
+ })
20
+ },
21
+ })
22
+
23
+ const characterCount = computed(() => htmlContent.value.length)
24
+ const isNearLimit = computed(() => characterCount.value > HTML_MAX_LENGTH * 0.9)
25
+ const isAtLimit = computed(() => characterCount.value >= HTML_MAX_LENGTH)
26
+
27
+ defineOptions({
28
+ name: 'HTMLSettings',
29
+ })
30
+ </script>
31
+
32
+ <template>
33
+ <div class="flex h-full flex-col p-4" :data-testid="htmlSettingsTestIds.container">
34
+ <!-- Monaco Editor - takes most of the space -->
35
+
36
+ <MonacoEditor
37
+ v-model="htmlContent"
38
+ language="html"
39
+ :line-numbers="'on'"
40
+ :minimap="false"
41
+ :word-wrap="'on'"
42
+ />
43
+
44
+ <div class="flex justify-end border-t pt-2 border-neutral-200">
45
+ <USlideover
46
+ close-icon="ph:x"
47
+ :close="{ 'data-testid': htmlSettingsTestIds.slideoverCloseButton }"
48
+ :title="$t('editor.email.blocks.raw_html.name')"
49
+ :ui="{
50
+ content: 'max-w-[90vw]',
51
+ }"
52
+ :data-testid="htmlSettingsTestIds.slideover"
53
+ >
54
+ <UButton
55
+ icon="ph:arrows-out-simple"
56
+ color="secondary"
57
+ :data-testid="htmlSettingsTestIds.expandButton"
58
+ />
59
+ <template #body>
60
+ <MonacoEditor
61
+ v-model="htmlContent"
62
+ language="html"
63
+ :line-numbers="'on'"
64
+ :minimap="false"
65
+ :word-wrap="'on'"
66
+ />
67
+ </template>
68
+ </USlideover>
69
+ </div>
70
+
71
+ <!-- Bottom Section -->
72
+ <div class="flex-shrink-0 space-y-4 p-4">
73
+ <!-- Warning Notice -->
74
+ <UAlert
75
+ color="neutral"
76
+ variant="subtle"
77
+ icon="i-heroicons-information-circle"
78
+ :title="$t('editor.email.blocks.raw_html.warning.title')"
79
+ :description="$t('editor.email.blocks.raw_html.warning.message')"
80
+ :data-testid="htmlSettingsTestIds.warningAlert"
81
+ >
82
+ </UAlert>
83
+ </div>
84
+ </div>
85
+ </template>
@@ -0,0 +1,53 @@
1
+ <script lang="ts" setup>
2
+ import type { HeaderBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ const emailEditorStore = useEmailEditorStore()
5
+
6
+ const props = defineProps<{
7
+ block: HeaderBlock
8
+ }>()
9
+
10
+ const resolvedSrc = computed(
11
+ () => props.block.settings.src || emailEditorStore.brand?.logo_url || null,
12
+ )
13
+
14
+ defineOptions({
15
+ name: 'HeaderBlock',
16
+ })
17
+
18
+ const alignmentClass: Record<string, string> = {
19
+ left: 'justify-start',
20
+ center: 'justify-center',
21
+ right: 'justify-end',
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <div
27
+ class="flex"
28
+ :class="alignmentClass[block.settings.alignment]"
29
+ :style="{ backgroundColor: block.settings.backgroundColor }"
30
+ :data-testid="headerBlockTestIds.container"
31
+ >
32
+ <div
33
+ v-if="!resolvedSrc"
34
+ class="flex w-full items-center justify-center rounded border border-dashed border-neutral-300 p-8"
35
+ :data-testid="headerBlockTestIds.placeholder"
36
+ >
37
+ <div class="text-center">
38
+ <div class="mb-2 text-4xl">🏷️</div>
39
+ <div class="text-sm text-neutral-500">
40
+ {{ $t('editor.email.blocks.header.no_logo') }}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <img
45
+ v-else
46
+ :src="resolvedSrc"
47
+ :alt="block.settings.alt"
48
+ class="max-w-full"
49
+ :style="{ width: `${block.settings.width}px` }"
50
+ :data-testid="headerBlockTestIds.logo"
51
+ />
52
+ </div>
53
+ </template>