@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,113 @@
1
+ <script lang="ts" setup>
2
+ import type { HeaderBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { DEFAULT_HEADER_SETTINGS } from '@dev.smartpricing/message-composer-utils/types'
4
+ import MediaUploadOrSelect from '@/components/Common/MediaUploadOrSelect.vue'
5
+ import ColorInput from '~/components/Common/ColorInput.vue'
6
+ import SectionHeader from '~/components/Common/SectionHeader.vue'
7
+ import type { TabsItem } from '#ui/types'
8
+
9
+ const props = defineProps<{
10
+ block: HeaderBlock
11
+ }>()
12
+
13
+ const emailEditorStore = useEmailEditorStore()
14
+ const { t } = useI18n()
15
+
16
+ const src = computed({
17
+ get: () => props.block.settings.src || emailEditorStore.brand?.logo_url || undefined,
18
+ set: (value) => updateSettings({ src: value }),
19
+ })
20
+
21
+ function updateSettings(updates: Partial<HeaderBlock['settings']>) {
22
+ emailEditorStore.updateBlock(props.block.id, {
23
+ settings: { ...props.block.settings, ...updates },
24
+ })
25
+ }
26
+
27
+ const alignmentOptions: TabsItem[] = [
28
+ { value: 'left', icon: 'ph:align-left' },
29
+ { value: 'center', icon: 'ph:align-center-horizontal' },
30
+ { value: 'right', icon: 'ph:align-right' },
31
+ ]
32
+
33
+ function resetSettings() {
34
+ emailEditorStore.updateBlock(props.block.id, {
35
+ settings: {
36
+ ...DEFAULT_HEADER_SETTINGS,
37
+ },
38
+ })
39
+ }
40
+
41
+ defineOptions({
42
+ name: 'HeaderSettings',
43
+ })
44
+ </script>
45
+
46
+ <template>
47
+ <div class="space-y-6 p-4" :data-testid="headerSettingsTestIds.container">
48
+ <!-- Logo Source -->
49
+ <div class="space-y-3">
50
+ <p class="paragraph-md-bold">
51
+ {{ $t('editor.email.blocks.header.settings.source') }}
52
+ </p>
53
+ <MediaUploadOrSelect v-model="src" media-type="image" />
54
+ </div>
55
+
56
+ <!-- Alt Text -->
57
+ <UFormField
58
+ :label="$t('editor.email.blocks.header.settings.alt')"
59
+ :data-testid="headerSettingsTestIds.altField"
60
+ >
61
+ <UInput
62
+ v-model="block.settings.alt"
63
+ :placeholder="$t('editor.email.blocks.header.settings.alt_placeholder')"
64
+ :data-testid="headerSettingsTestIds.altInput"
65
+ />
66
+ </UFormField>
67
+
68
+ <!-- Style Section -->
69
+ <div class="space-y-3">
70
+ <SectionHeader :title="$t('editor.email.blocks.header.settings.style')" />
71
+
72
+ <!-- Logo Width -->
73
+ <UFormField
74
+ :label="$t('editor.email.blocks.header.settings.width')"
75
+ :data-testid="headerSettingsTestIds.widthField"
76
+ >
77
+ <USlider
78
+ v-model="block.settings.width"
79
+ :min="100"
80
+ :max="300"
81
+ :step="10"
82
+ :data-testid="headerSettingsTestIds.widthSlider"
83
+ />
84
+ </UFormField>
85
+
86
+ <!-- Alignment -->
87
+ <UTabs
88
+ v-model="block.settings.alignment"
89
+ :items="alignmentOptions"
90
+ :content="false"
91
+ size="sm"
92
+ :data-testid="headerSettingsTestIds.alignmentTabs"
93
+ />
94
+
95
+ <!-- Background Color -->
96
+ <ColorInput
97
+ v-model="block.settings.backgroundColor"
98
+ :label="$t('editor.email.blocks.header.settings.background')"
99
+ />
100
+ </div>
101
+
102
+ <!-- Reset button -->
103
+ <UButton
104
+ variant="outline"
105
+ color="neutral"
106
+ icon="ph:arrow-counter-clockwise"
107
+ block
108
+ @click="resetSettings"
109
+ >
110
+ {{ $t('editor.email.blocks.header.settings.reset') }}
111
+ </UButton>
112
+ </div>
113
+ </template>
@@ -0,0 +1,61 @@
1
+ <script lang="ts" setup>
2
+ import type { HeadingBlock } 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: HeadingBlock
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: 'HeadingBlock',
36
+ })
37
+ </script>
38
+
39
+ <template>
40
+ <div
41
+ class="rounded p-2 relative"
42
+ :style="{ backgroundColor: block.settings.backgroundColor }"
43
+ :data-testid="headingBlockTestIds.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,156 @@
1
+ <script lang="ts" setup>
2
+ import type { HeadingBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import {
4
+ HEADING_TAGS,
5
+ TAGS_DEFAULT_FONT_SIZE,
6
+ DEFAULT_HEADING_SETTINGS,
7
+ } from '@dev.smartpricing/message-composer-utils/types'
8
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
9
+ import { computed } from 'vue'
10
+ import { useAppContextStore } from '@/stores/appContext'
11
+ import ColorInput from '~/components/Common/ColorInput.vue'
12
+ import FontFamilySelect from '~/components/Common/FontFamilySelect.vue'
13
+ import SectionHeader from '~/components/Common/SectionHeader.vue'
14
+
15
+ const props = defineProps<{
16
+ block: HeadingBlock
17
+ }>()
18
+
19
+ const emailEditorStore = useEmailEditorStore()
20
+ const appContext = useAppContextStore()
21
+ const { context: generateContext } = useEmailBlockContext(props.block.id)
22
+ const { t } = useI18n()
23
+
24
+ const content = computed({
25
+ get: () => props.block.settings.content,
26
+ set: (value) => {
27
+ emailEditorStore.updateBlock(props.block.id, {
28
+ settings: { ...props.block.settings, content: value },
29
+ })
30
+ },
31
+ })
32
+
33
+ const typeOptions = HEADING_TAGS.slice(0, 3).map((heading) => ({
34
+ value: heading,
35
+ label: t(`editor.email.blocks.text_types.${heading}`),
36
+ }))
37
+
38
+ function resetTextStyle() {
39
+ emailEditorStore.updateBlock(props.block.id, {
40
+ settings: {
41
+ ...props.block.settings,
42
+ fontFamily: DEFAULT_HEADING_SETTINGS.fontFamily,
43
+ fontSize: DEFAULT_HEADING_SETTINGS.fontSize,
44
+ tag: DEFAULT_HEADING_SETTINGS.tag,
45
+ },
46
+ })
47
+ }
48
+
49
+ function resetColors() {
50
+ emailEditorStore.updateBlock(props.block.id, {
51
+ settings: {
52
+ ...props.block.settings,
53
+ textColor: DEFAULT_HEADING_SETTINGS.textColor,
54
+ backgroundColor: DEFAULT_HEADING_SETTINGS.backgroundColor,
55
+ },
56
+ })
57
+ }
58
+
59
+ function updateFontSize(value: (typeof HEADING_TAGS)[number]) {
60
+ const defaultFontSize = TAGS_DEFAULT_FONT_SIZE[value]
61
+ if (defaultFontSize) {
62
+ props.block.settings.fontSize = defaultFontSize
63
+ }
64
+ }
65
+
66
+ defineOptions({
67
+ name: 'HeadingSettings',
68
+ })
69
+ </script>
70
+
71
+ <template>
72
+ <div class="space-y-6 p-4" :data-testid="headingSettingsTestIds.container">
73
+ <!-- Content Editor -->
74
+ <div class="space-y-3">
75
+ <p class="paragraph-md-bold">
76
+ {{ $t('editor.email.blocks.heading.settings.content') }}
77
+ </p>
78
+ <RichTextEditor
79
+ v-model:text="content"
80
+ preset="email"
81
+ bold
82
+ italic
83
+ underline
84
+ alignment
85
+ link
86
+ emoji
87
+ improve
88
+ generate
89
+ generate-widget-type="heading"
90
+ :generate-context="generateContext"
91
+ dynamic-values
92
+ />
93
+ </div>
94
+
95
+ <!-- Text Style Section -->
96
+ <div class="space-y-3">
97
+ <SectionHeader
98
+ :title="$t('editor.email.blocks.heading.settings.textStyle')"
99
+ show-reset
100
+ @reset="resetTextStyle"
101
+ />
102
+
103
+ <!-- Type -->
104
+
105
+ <USelect
106
+ @update:model-value="updateFontSize"
107
+ v-model="block.settings.tag"
108
+ :items="typeOptions"
109
+ :data-testid="headingSettingsTestIds.tagSelect"
110
+ />
111
+
112
+ <!-- Font Family & Size Row -->
113
+ <div class="grid grid-cols-2 gap-3">
114
+ <FontFamilySelect
115
+ v-model="block.settings.fontFamily"
116
+ :label="$t('editor.email.blocks.heading.settings.font')"
117
+ :default-value="emailEditorStore.resolvedSettings.fontFamily"
118
+ />
119
+ <UFormField
120
+ :label="$t('editor.email.blocks.heading.settings.size')"
121
+ :data-testid="headingSettingsTestIds.fontSizeField"
122
+ >
123
+ <UFieldGroup class="w-full">
124
+ <UInputNumber
125
+ v-model="block.settings.fontSize"
126
+ :data-testid="headingSettingsTestIds.fontSizeInput"
127
+ />
128
+ <UBadge color="neutral" variant="outline">px</UBadge>
129
+ </UFieldGroup>
130
+ </UFormField>
131
+ </div>
132
+ </div>
133
+
134
+ <!-- Color Section -->
135
+ <div class="space-y-3">
136
+ <SectionHeader
137
+ :title="$t('editor.email.blocks.heading.settings.color')"
138
+ show-reset
139
+ @reset="resetColors"
140
+ />
141
+
142
+ <!-- Text Color -->
143
+ <ColorInput
144
+ v-model="block.settings.textColor"
145
+ :label="$t('editor.email.blocks.heading.settings.text')"
146
+ :default-value="emailEditorStore.resolvedSettings.textColor"
147
+ />
148
+
149
+ <!-- Background Color -->
150
+ <ColorInput
151
+ v-model="block.settings.backgroundColor"
152
+ :label="$t('editor.email.blocks.heading.settings.backgroundBlock')"
153
+ />
154
+ </div>
155
+ </div>
156
+ </template>
@@ -0,0 +1,49 @@
1
+ <script lang="ts" setup>
2
+ import type { ImageBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ const props = defineProps<{
5
+ block: ImageBlock
6
+ }>()
7
+
8
+ defineOptions({
9
+ name: 'ImageBlock',
10
+ })
11
+ </script>
12
+
13
+ <template>
14
+ <div class="text-center" :data-testid="imageBlockTestIds.container">
15
+ <div
16
+ v-if="!block.settings.src"
17
+ class="flex items-center justify-center rounded border border-dashed border-neutral-300 p-8"
18
+ :data-testid="imageBlockTestIds.placeholder"
19
+ >
20
+ <div class="text-center">
21
+ <div class="mb-2 text-4xl">🖼️</div>
22
+ <div class="text-sm text-neutral-500">
23
+ {{ $t('editor.email.blocks.image.no_image') }}
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <a
28
+ v-else-if="block.settings.href"
29
+ :href="block.settings.href"
30
+ target="_blank"
31
+ rel="noopener"
32
+ :data-testid="imageBlockTestIds.link"
33
+ >
34
+ <img
35
+ :src="block.settings.src"
36
+ alt=""
37
+ class="mx-auto max-w-full"
38
+ :data-testid="imageBlockTestIds.image"
39
+ />
40
+ </a>
41
+ <img
42
+ v-else
43
+ :src="block.settings.src"
44
+ alt=""
45
+ class="mx-auto max-w-full"
46
+ :data-testid="imageBlockTestIds.image"
47
+ />
48
+ </div>
49
+ </template>
@@ -0,0 +1,53 @@
1
+ <script lang="ts" setup>
2
+ import type { ImageBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import MediaUploadOrSelect from '@/components/Common/MediaUploadOrSelect.vue'
4
+
5
+ const props = defineProps<{
6
+ block: ImageBlock
7
+ }>()
8
+
9
+ const emailEditorStore = useEmailEditorStore()
10
+
11
+ const imageSrc = computed({
12
+ get: () => props.block.settings.src,
13
+ set: (value) => updateSettings({ src: value }),
14
+ })
15
+
16
+ function updateSettings(updates: Partial<ImageBlock['settings']>) {
17
+ emailEditorStore.updateBlock(props.block.id, {
18
+ settings: { ...props.block.settings, ...updates },
19
+ })
20
+ }
21
+
22
+ defineOptions({
23
+ name: 'ImageSettings',
24
+ })
25
+ </script>
26
+
27
+ <template>
28
+ <div class="space-y-6 p-4" :data-testid="imageSettingsTestIds.container">
29
+ <!-- Image Source Section -->
30
+ <div class="space-y-3">
31
+ <p class="paragraph-md-bold">
32
+ {{ $t('editor.email.blocks.image.settings.source') }}
33
+ </p>
34
+
35
+ <MediaUploadOrSelect v-model="imageSrc" media-type="image" />
36
+ </div>
37
+
38
+ <!-- Link Section -->
39
+
40
+ <UFormField
41
+ :label="$t('editor.email.blocks.image.settings.link')"
42
+ :help="$t('editor.email.blocks.image.href_help')"
43
+ :data-testid="imageSettingsTestIds.linkField"
44
+ >
45
+ <UInput
46
+ v-model="block.settings.href"
47
+ type="url"
48
+ :placeholder="$t('editor.email.blocks.image.settings.href_placeholder')"
49
+ :data-testid="imageSettingsTestIds.linkInput"
50
+ />
51
+ </UFormField>
52
+ </div>
53
+ </template>
@@ -0,0 +1,155 @@
1
+ <script lang="ts" setup>
2
+ import type { EmailBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import SpacerBlock from './SpacerBlock.vue'
4
+ import DividerBlock from './DividerBlock.vue'
5
+ import ParagraphBlock from './ParagraphBlock.vue'
6
+ import HeadingBlock from './HeadingBlock.vue'
7
+ import HTMLBlock from './HTMLBlock.vue'
8
+ import ImageBlock from './ImageBlock.vue'
9
+ import ButtonBlock from './ButtonBlock.vue'
10
+ import DiscountCodeBlock from './DiscountCodeBlock.vue'
11
+ import GridBlock from './GridBlock.vue'
12
+ import FooterBlock from './FooterBlock.vue'
13
+ import UnsubscribeLinkBlock from './UnsubscribeLinkBlock.vue'
14
+ import SocialsBlock from './SocialsBlock.vue'
15
+ import HeaderBlock from './HeaderBlock.vue'
16
+ import TopProductsBlock from './TopProductsBlock.vue'
17
+
18
+ interface Props {
19
+ block: EmailBlock
20
+ isSelected: boolean
21
+ showDragHandle?: boolean
22
+ showDuplicate?: boolean
23
+ showDelete?: boolean
24
+ showLabel?: boolean
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ showDragHandle: true,
29
+ showDuplicate: true,
30
+ showDelete: true,
31
+ showLabel: true,
32
+ })
33
+
34
+ const emit = defineEmits<{
35
+ click: [blockId: string]
36
+ duplicate: [blockId: string, event: Event]
37
+ delete: [blockId: string, event: Event]
38
+ }>()
39
+
40
+ const { t } = useI18n()
41
+
42
+ // Block component registry
43
+ const blockComponents: Partial<Record<EmailBlock['type'], any>> = {
44
+ spacer: SpacerBlock,
45
+ divider: DividerBlock,
46
+ paragraph: ParagraphBlock,
47
+ heading: HeadingBlock,
48
+ raw_html: HTMLBlock,
49
+ image: ImageBlock,
50
+ button: ButtonBlock,
51
+ discount: DiscountCodeBlock,
52
+ grid: GridBlock,
53
+ footer: FooterBlock,
54
+ unsubscribe_link: UnsubscribeLinkBlock,
55
+ socials: SocialsBlock,
56
+ header: HeaderBlock,
57
+ top_products: TopProductsBlock,
58
+ }
59
+
60
+ function getBlockName(type: EmailBlock['type']): string {
61
+ return t(`editor.email.blocks.${type}.name`) || type
62
+ }
63
+
64
+ function handleClick() {
65
+ emit('click', props.block.id)
66
+ }
67
+
68
+ function handleDuplicate(event: Event) {
69
+ event.stopPropagation()
70
+ emit('duplicate', props.block.id, event)
71
+ }
72
+
73
+ function handleDelete(event: Event) {
74
+ event.stopPropagation()
75
+ emit('delete', props.block.id, event)
76
+ }
77
+
78
+ defineOptions({
79
+ name: 'EmailEditorIndexBlock',
80
+ })
81
+ </script>
82
+
83
+ <template>
84
+ <UPopover
85
+ mode="hover"
86
+ :content="{ side: 'top', align: 'start', sideOffset: 2 }"
87
+ :ui="{ content: 'bg-neutral-800 text-white text-xs rounded px-2 py-1' }"
88
+ >
89
+ <template #content>
90
+ <div>
91
+ {{ getBlockName(block.type) }}
92
+ </div>
93
+ </template>
94
+
95
+ <div
96
+ :data-block-id="block.id"
97
+ :data-block-type="block.type"
98
+ :data-testid="`${indexBlockTestIds.container}-${block.id}`"
99
+ class="group relative rounded border-2 transition-colors"
100
+ :class="{
101
+ 'border-primary-500': isSelected,
102
+ 'border-transparent hover:border-neutral-300': !isSelected,
103
+ 'cursor-move': showDragHandle,
104
+ }"
105
+ @click.stop="handleClick"
106
+ >
107
+ <UPopover mode="hover" :content="{ side: 'right' }">
108
+ <!-- Block render component -->
109
+ <div class="">
110
+ <component
111
+ :is="blockComponents[block.type]"
112
+ v-if="blockComponents[block.type]"
113
+ :block="block"
114
+ />
115
+ <div
116
+ v-else
117
+ class="rounded border border-dashed border-neutral-300 p-4 text-center text-sm text-neutral-500"
118
+ >
119
+ {{ $t('editor.email.canvas.blockNotImplemented', { type: block.type }) }}
120
+ </div>
121
+ </div>
122
+
123
+ <template #content>
124
+ <div
125
+ class="bg-white rounded shadow-sm border border-neutral-200 overflow-hidden flex flex-col"
126
+ >
127
+ <!-- <div class="p-1 flex items-center justify-center" v-if="showDragHandle">
128
+ <UIcon size="sm" name="ph:dots-six-vertical-light" />
129
+ </div> -->
130
+ <UButton
131
+ v-if="showDuplicate"
132
+ size="sm"
133
+ variant="ghost"
134
+ color="neutral"
135
+ icon="ph:copy"
136
+ square
137
+ :data-testid="`${indexBlockTestIds.duplicateButton}-${block.id}`"
138
+ @click="handleDuplicate"
139
+ />
140
+ <UButton
141
+ v-if="showDelete"
142
+ size="sm"
143
+ variant="ghost"
144
+ color="neutral"
145
+ icon="ph:trash"
146
+ square
147
+ :data-testid="`${indexBlockTestIds.deleteButton}-${block.id}`"
148
+ @click="handleDelete"
149
+ />
150
+ </div>
151
+ </template>
152
+ </UPopover>
153
+ </div>
154
+ </UPopover>
155
+ </template>
@@ -0,0 +1,61 @@
1
+ <script lang="ts" setup>
2
+ import type { ParagraphBlock } 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: ParagraphBlock
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: 'ParagraphBlock',
36
+ })
37
+ </script>
38
+
39
+ <template>
40
+ <div
41
+ class="rounded p-2 relative"
42
+ :style="{ backgroundColor: block.settings.backgroundColor }"
43
+ :data-testid="paragraphBlockTestIds.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>