@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,157 @@
1
+ <script lang="ts" setup>
2
+ import type { ParagraphBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import {
4
+ TAGS_DEFAULT_FONT_SIZE,
5
+ TEXT_TAGS,
6
+ DEFAULT_PARAGRAPH_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: ParagraphBlock
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 = TEXT_TAGS.map((tag) => ({
34
+ value: tag,
35
+ label: t(`editor.email.blocks.text_types.${tag}`),
36
+ }))
37
+
38
+ function resetTextStyle() {
39
+ emailEditorStore.updateBlock(props.block.id, {
40
+ settings: {
41
+ ...props.block.settings,
42
+ fontFamily: DEFAULT_PARAGRAPH_SETTINGS.fontFamily,
43
+ fontSize: DEFAULT_PARAGRAPH_SETTINGS.fontSize,
44
+ tag: DEFAULT_PARAGRAPH_SETTINGS.tag,
45
+ },
46
+ })
47
+ }
48
+
49
+ function resetColors() {
50
+ emailEditorStore.updateBlock(props.block.id, {
51
+ settings: {
52
+ ...props.block.settings,
53
+ textColor: DEFAULT_PARAGRAPH_SETTINGS.textColor,
54
+ backgroundColor: DEFAULT_PARAGRAPH_SETTINGS.backgroundColor,
55
+ },
56
+ })
57
+ }
58
+
59
+ function updateFontSize(value: (typeof TEXT_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: 'ParagraphSettings',
68
+ })
69
+ </script>
70
+
71
+ <template>
72
+ <div class="space-y-6 p-4" :data-testid="paragraphSettingsTestIds.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
+ lists
85
+ horizontal-rule
86
+ alignment
87
+ link
88
+ emoji
89
+ improve
90
+ generate
91
+ generate-widget-type="paragraph"
92
+ :generate-context="generateContext"
93
+ dynamic-values
94
+ />
95
+ </div>
96
+
97
+ <!-- Text Style Section -->
98
+ <div class="space-y-3">
99
+ <SectionHeader
100
+ :title="$t('editor.email.blocks.heading.settings.textStyle')"
101
+ show-reset
102
+ @reset="resetTextStyle"
103
+ />
104
+
105
+ <!-- Type -->
106
+ <USelect
107
+ v-model="block.settings.tag"
108
+ :items="typeOptions"
109
+ :data-testid="paragraphSettingsTestIds.tagSelect"
110
+ @update:model-value="updateFontSize"
111
+ />
112
+
113
+ <!-- Font Family & Size Row -->
114
+ <div class="grid grid-cols-2 gap-3">
115
+ <FontFamilySelect
116
+ v-model="block.settings.fontFamily"
117
+ :label="$t('editor.email.blocks.heading.settings.font')"
118
+ :default-value="emailEditorStore.resolvedSettings.fontFamily"
119
+ />
120
+ <UFormField
121
+ :label="$t('editor.email.blocks.heading.settings.size')"
122
+ :data-testid="paragraphSettingsTestIds.fontSizeField"
123
+ >
124
+ <UFieldGroup class="w-full">
125
+ <UInputNumber
126
+ v-model="block.settings.fontSize"
127
+ :data-testid="paragraphSettingsTestIds.fontSizeInput"
128
+ />
129
+ <UBadge color="neutral" variant="outline">px</UBadge>
130
+ </UFieldGroup>
131
+ </UFormField>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Color Section -->
136
+ <div class="space-y-3">
137
+ <SectionHeader
138
+ :title="$t('editor.email.blocks.heading.settings.color')"
139
+ show-reset
140
+ @reset="resetColors"
141
+ />
142
+
143
+ <!-- Text Color -->
144
+ <ColorInput
145
+ v-model="block.settings.textColor"
146
+ :label="$t('editor.email.blocks.heading.settings.text')"
147
+ :default-value="emailEditorStore.resolvedSettings.textColor"
148
+ />
149
+
150
+ <!-- Background Color -->
151
+ <ColorInput
152
+ v-model="block.settings.backgroundColor"
153
+ :label="$t('editor.email.blocks.heading.settings.backgroundBlock')"
154
+ />
155
+ </div>
156
+ </div>
157
+ </template>
@@ -0,0 +1,63 @@
1
+ <script lang="ts" setup>
2
+ import type { SocialsBlock, SocialPlatform } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { SOCIAL_ICONS, SOCIAL_PLATFORMS } from '@dev.smartpricing/message-composer-utils/types'
4
+
5
+ const emailEditorStore = useEmailEditorStore()
6
+
7
+ const props = defineProps<{
8
+ block: SocialsBlock
9
+ }>()
10
+
11
+ const resolvedSocials = computed(() => {
12
+ const blockSocials = props.block.settings.socials
13
+ const brandSocials = emailEditorStore.brand?.default_socials
14
+ if (!brandSocials) return blockSocials
15
+
16
+ const result = { ...blockSocials }
17
+ for (const platform of SOCIAL_PLATFORMS) {
18
+ const bs = result[platform]
19
+ if (!bs?.enabled) continue
20
+ // Enabled + no block URL → cascade from brand
21
+ if (!bs.url && brandSocials[platform]) {
22
+ result[platform] = { ...bs, url: brandSocials[platform]! }
23
+ }
24
+ }
25
+ return result
26
+ })
27
+
28
+ const enabledSocials = computed(() =>
29
+ SOCIAL_PLATFORMS.filter((platform) => resolvedSocials.value[platform].enabled),
30
+ )
31
+
32
+ defineOptions({
33
+ name: 'SocialsBlock',
34
+ })
35
+ </script>
36
+
37
+ <template>
38
+ <div
39
+ class="rounded p-2 relative"
40
+ :style="{ backgroundColor: block.settings.backgroundColor }"
41
+ :data-testid="socialsBlockTestIds.container"
42
+ >
43
+ <div v-if="enabledSocials.length > 0" class="flex items-center justify-center gap-3">
44
+ <a
45
+ v-for="platform in enabledSocials"
46
+ :key="platform"
47
+ :href="resolvedSocials[platform].url || '#'"
48
+ target="_blank"
49
+ rel="noopener noreferrer"
50
+ :data-testid="`${socialsBlockTestIds.link}-${platform}`"
51
+ >
52
+ <img :src="SOCIAL_ICONS[platform]" :alt="platform" width="24" height="24" />
53
+ </a>
54
+ </div>
55
+ <div
56
+ v-else
57
+ class="w-full flex items-center justify-center text-neutral-400 text-sm py-2"
58
+ :data-testid="socialsBlockTestIds.placeholder"
59
+ >
60
+ {{ $t('editor.email.blocks.socials.placeholder') }}
61
+ </div>
62
+ </div>
63
+ </template>
@@ -0,0 +1,117 @@
1
+ <script lang="ts" setup>
2
+ import type { SocialsBlock, SocialPlatform } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { SOCIAL_PLATFORMS } from '@dev.smartpricing/message-composer-utils/types'
4
+ import { applySocialDefaults } from '~/utils/socialDefaults'
5
+ import ColorInput from '~/components/Common/ColorInput.vue'
6
+ import SectionHeader from '~/components/Common/SectionHeader.vue'
7
+
8
+ const props = defineProps<{
9
+ block: SocialsBlock
10
+ }>()
11
+
12
+ const emailEditorStore = useEmailEditorStore()
13
+ const { t } = useI18n()
14
+
15
+ const brandSocials = computed(() => emailEditorStore.brand?.default_socials ?? null)
16
+
17
+ function brandUrl(platform: SocialPlatform): string | undefined {
18
+ return brandSocials.value?.[platform] || undefined
19
+ }
20
+
21
+ function toggleSocial(platform: SocialPlatform) {
22
+ const current = props.block.settings.socials[platform]
23
+ emailEditorStore.updateBlock(props.block.id, {
24
+ settings: {
25
+ ...props.block.settings,
26
+ socials: {
27
+ ...props.block.settings.socials,
28
+ [platform]: { ...current, enabled: !current.enabled },
29
+ },
30
+ },
31
+ })
32
+ }
33
+
34
+ function updateSocialUrl(platform: SocialPlatform, url: string) {
35
+ emailEditorStore.updateBlock(props.block.id, {
36
+ settings: {
37
+ ...props.block.settings,
38
+ socials: {
39
+ ...props.block.settings.socials,
40
+ [platform]: { ...props.block.settings.socials[platform], url },
41
+ },
42
+ },
43
+ })
44
+ }
45
+
46
+ function resetToDefaults() {
47
+ const brandSocials = emailEditorStore.brand?.default_socials ?? null
48
+ emailEditorStore.updateBlock(props.block.id, {
49
+ settings: {
50
+ ...props.block.settings,
51
+ socials: applySocialDefaults(brandSocials),
52
+ },
53
+ })
54
+ }
55
+
56
+ function socialLabel(platform: SocialPlatform): string {
57
+ return t(`editor.email.blocks.socials.platforms.${platform}`)
58
+ }
59
+
60
+ defineOptions({
61
+ name: 'SocialsSettings',
62
+ })
63
+ </script>
64
+
65
+ <template>
66
+ <div class="space-y-6 p-4" :data-testid="socialsSettingsTestIds.container">
67
+ <!-- Social Links -->
68
+ <div class="space-y-3">
69
+ <SectionHeader :title="$t('editor.email.blocks.socials.settings.links')" />
70
+
71
+ <div v-for="platform in SOCIAL_PLATFORMS" :key="platform" class="space-y-2">
72
+ <div class="flex items-center gap-2">
73
+ <USwitch
74
+ :model-value="block.settings.socials[platform].enabled"
75
+ :data-testid="`${socialsSettingsTestIds.toggleSwitch}-${platform}`"
76
+ @update:model-value="toggleSocial(platform)"
77
+ />
78
+ <span class="paragraph-sm capitalize">{{ socialLabel(platform) }}</span>
79
+ </div>
80
+ <template v-if="block.settings.socials[platform].enabled">
81
+ <UInput
82
+ :model-value="block.settings.socials[platform].url"
83
+ :placeholder="brandUrl(platform) ?? 'https://'"
84
+ :data-testid="`${socialsSettingsTestIds.urlInput}-${platform}`"
85
+ @update:model-value="updateSocialUrl(platform, $event as string)"
86
+ />
87
+ <p
88
+ v-if="!block.settings.socials[platform].url && brandUrl(platform)"
89
+ class="text-xs text-neutral-400 pl-1"
90
+ >
91
+ {{ $t('editor.email.blocks.socials.settings.from_brand', { url: brandUrl(platform) }) }}
92
+ </p>
93
+ </template>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Reset to defaults -->
98
+ <UButton
99
+ variant="outline"
100
+ color="neutral"
101
+ size="sm"
102
+ icon="ph:arrow-counter-clockwise"
103
+ :label="$t('common.actions.reset_defaults')"
104
+ @click="resetToDefaults"
105
+ />
106
+
107
+ <!-- Background Color -->
108
+ <div class="space-y-3">
109
+ <SectionHeader :title="$t('editor.email.blocks.heading.settings.color')" />
110
+
111
+ <ColorInput
112
+ v-model="block.settings.backgroundColor"
113
+ :label="$t('editor.email.blocks.heading.settings.backgroundBlock')"
114
+ />
115
+ </div>
116
+ </div>
117
+ </template>
@@ -0,0 +1,37 @@
1
+ <script lang="ts" setup>
2
+ import type { SpacerBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { SPACER_SIZE_PRESETS } from '@dev.smartpricing/message-composer-utils/types'
4
+ import { computed } from 'vue'
5
+
6
+ const props = defineProps<{
7
+ block: SpacerBlock
8
+ }>()
9
+
10
+ const height = computed(() => {
11
+ return props.block.settings.height
12
+ })
13
+
14
+ defineOptions({
15
+ name: 'SpacerBlock',
16
+ })
17
+ </script>
18
+
19
+ <template>
20
+ <div
21
+ class="relative"
22
+ :style="{
23
+ height: `${height}px`,
24
+ backgroundColor: block.settings.backgroundColor,
25
+ }"
26
+ :data-testid="spacerBlockTestIds.container"
27
+ >
28
+ <!-- Visual indicator -->
29
+ <div
30
+ class="absolute inset-0 flex items-center justify-center border border-dashed border-neutral-300 bg-opacity-50"
31
+ >
32
+ <span class="text-xs text-neutral-400">
33
+ {{ $t('editor.email.blocks.spacer.label', { height }) }}
34
+ </span>
35
+ </div>
36
+ </div>
37
+ </template>
@@ -0,0 +1,95 @@
1
+ <script lang="ts" setup>
2
+ import type { SpacerBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import {
4
+ SPACER_SIZE_PRESETS,
5
+ SPACER_MIN_HEIGHT,
6
+ SPACER_MAX_HEIGHT,
7
+ DEFAULT_SPACER_SETTINGS,
8
+ } from '@dev.smartpricing/message-composer-utils/types'
9
+ import ColorInput from '~/components/Common/ColorInput.vue'
10
+
11
+ const props = defineProps<{
12
+ block: SpacerBlock
13
+ }>()
14
+
15
+ const { t } = useI18n()
16
+ const emailEditorStore = useEmailEditorStore()
17
+
18
+ const sizePresetItems = computed(() =>
19
+ Object.entries(SPACER_SIZE_PRESETS).map(([key, value]) => ({
20
+ value,
21
+ label: `${t(`editor.email.blocks.spacer.settings.sizePresets.${key}`)} (${value}px)`,
22
+ })),
23
+ )
24
+
25
+ function resetAll() {
26
+ emailEditorStore.updateBlock(props.block.id, {
27
+ settings: { ...DEFAULT_SPACER_SETTINGS },
28
+ })
29
+ }
30
+
31
+ defineOptions({
32
+ name: 'SpacerSettings',
33
+ })
34
+ </script>
35
+
36
+ <template>
37
+ <div class="space-y-6 p-4" :data-testid="spacerSettingsTestIds.container">
38
+ <!-- Size Preset -->
39
+ <div class="space-y-3">
40
+ <p class="paragraph-md-bold">
41
+ {{ $t('editor.email.blocks.spacer.settings.size') }}
42
+ </p>
43
+ <UFormField :data-testid="spacerSettingsTestIds.sizePresetField">
44
+ <USelect
45
+ :items="sizePresetItems"
46
+ :disabled="block.settings.customSize"
47
+ v-model="block.settings.height"
48
+ :data-testid="spacerSettingsTestIds.sizePresetSelect"
49
+ />
50
+ </UFormField>
51
+
52
+ <!-- Custom Height Toggle -->
53
+ <USwitch
54
+ v-model="block.settings.customSize"
55
+ :label="$t('editor.email.blocks.spacer.settings.customSize')"
56
+ :data-testid="spacerSettingsTestIds.customSizeSwitch"
57
+ />
58
+
59
+ <!-- Custom Height Input -->
60
+
61
+ <UFieldGroup v-if="block.settings.customSize" class="w-full">
62
+ <UInputNumber
63
+ v-model="block.settings.height"
64
+ :min="SPACER_MIN_HEIGHT"
65
+ :max="SPACER_MAX_HEIGHT"
66
+ :data-testid="spacerSettingsTestIds.customSizeInput"
67
+ />
68
+
69
+ <UBadge color="neutral" variant="outline">px</UBadge>
70
+ </UFieldGroup>
71
+ </div>
72
+
73
+ <div>
74
+ <p class="paragraph-md-bold pb-3">
75
+ {{ $t('editor.email.blocks.spacer.settings.backgroundColor') }}
76
+ </p>
77
+ <ColorInput
78
+ v-model="block.settings.backgroundColor"
79
+ :label="$t('editor.email.blocks.spacer.settings.backgroundColor')"
80
+ />
81
+ </div>
82
+
83
+ <!-- Reset All Button -->
84
+ <UButton
85
+ variant="outline"
86
+ color="neutral"
87
+ block
88
+ icon="ph:arrow-counter-clockwise"
89
+ :data-testid="spacerSettingsTestIds.resetButton"
90
+ @click="resetAll"
91
+ >
92
+ {{ $t('editor.email.common.reset') }}
93
+ </UButton>
94
+ </div>
95
+ </template>
@@ -0,0 +1,47 @@
1
+ <script lang="ts" setup>
2
+ import type { TopProductsBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ defineProps<{
5
+ block: TopProductsBlock
6
+ }>()
7
+
8
+ defineOptions({
9
+ name: 'TopProductsBlock',
10
+ })
11
+ </script>
12
+
13
+ <template>
14
+ <div class="rounded-lg p-6" :style="{ backgroundColor: block.settings.backgroundColor }">
15
+ <div
16
+ v-if="!block.settings.storeId"
17
+ class="flex items-center justify-center rounded border border-dashed border-neutral-300 bg-white p-8"
18
+ >
19
+ <div class="text-center">
20
+ <div class="mb-2 text-4xl">
21
+ <UIcon name="ph:storefront" />
22
+ </div>
23
+ <div class="text-sm text-neutral-500">
24
+ {{ $t('editor.email.blocks.top_products.no_store_id') }}
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Preview placeholder cards -->
30
+ <div v-else class="space-y-2">
31
+ <div
32
+ v-for="i in block.settings.maxProducts"
33
+ :key="i"
34
+ class="flex gap-3 rounded-xl border-2 border-neutral-200 bg-white p-1"
35
+ >
36
+ <div class="h-16 w-16 shrink-0 rounded bg-neutral-100" />
37
+ <div class="flex flex-col justify-center gap-1 py-2">
38
+ <div class="h-3 w-24 rounded bg-neutral-200" />
39
+ <div class="h-2 w-16 rounded bg-neutral-100" />
40
+ </div>
41
+ </div>
42
+ <div class="pt-1 text-center text-xs text-neutral-400">
43
+ {{ block.settings.storeId }}
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </template>
@@ -0,0 +1,87 @@
1
+ <script lang="ts" setup>
2
+ import type { TopProductsBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { DEFAULT_TOP_PRODUCTS_SETTINGS } from '@dev.smartpricing/message-composer-utils/types'
4
+ import ColorInput from '~/components/Common/ColorInput.vue'
5
+ import SectionHeader from '~/components/Common/SectionHeader.vue'
6
+
7
+ const props = defineProps<{
8
+ block: TopProductsBlock
9
+ }>()
10
+
11
+ const emailEditorStore = useEmailEditorStore()
12
+
13
+ const maxProductsOptions = [
14
+ { value: '1', label: '1' },
15
+ { value: '2', label: '2' },
16
+ { value: '3', label: '3' },
17
+ { value: '4', label: '4' },
18
+ { value: '5', label: '5' },
19
+ ]
20
+
21
+ const selectedMaxProducts = computed({
22
+ get: () => String(props.block.settings.maxProducts),
23
+ set: (value) => updateSettings({ maxProducts: Number(value) }),
24
+ })
25
+
26
+ function updateSettings(updates: Partial<TopProductsBlock['settings']>) {
27
+ emailEditorStore.updateBlock(props.block.id, {
28
+ settings: { ...props.block.settings, ...updates },
29
+ })
30
+ }
31
+
32
+ function resetSettings() {
33
+ emailEditorStore.updateBlock(props.block.id, {
34
+ settings: {
35
+ ...props.block.settings,
36
+ maxProducts: DEFAULT_TOP_PRODUCTS_SETTINGS.maxProducts,
37
+ backgroundColor: DEFAULT_TOP_PRODUCTS_SETTINGS.backgroundColor,
38
+ },
39
+ })
40
+ }
41
+
42
+ defineOptions({
43
+ name: 'TopProductsSettings',
44
+ })
45
+ </script>
46
+
47
+ <template>
48
+ <div class="space-y-6 p-4">
49
+ <!-- Link -->
50
+ <UFormField
51
+ :label="$t('editor.email.blocks.top_products.settings.store_id')"
52
+ :help="$t('editor.email.blocks.top_products.settings.store_id_help')"
53
+ >
54
+ <UInput
55
+ :model-value="block.settings.storeId"
56
+ :placeholder="$t('editor.email.blocks.top_products.settings.store_id_placeholder')"
57
+ @update:model-value="updateSettings({ storeId: $event as string })"
58
+ />
59
+ </UFormField>
60
+
61
+ <!-- Style Section -->
62
+ <div class="space-y-3">
63
+ <SectionHeader
64
+ :title="$t('editor.email.blocks.top_products.settings.style')"
65
+ show-reset
66
+ @reset="resetSettings"
67
+ />
68
+
69
+ <!-- Max Products -->
70
+ <UFormField :label="$t('editor.email.blocks.top_products.settings.max_products')">
71
+ <UTabs
72
+ v-model="selectedMaxProducts"
73
+ :items="maxProductsOptions"
74
+ :content="false"
75
+ size="sm"
76
+ />
77
+ </UFormField>
78
+
79
+ <!-- Background Color -->
80
+ <ColorInput
81
+ :model-value="block.settings.backgroundColor"
82
+ :label="$t('editor.email.blocks.top_products.settings.background')"
83
+ @update:model-value="updateSettings({ backgroundColor: $event })"
84
+ />
85
+ </div>
86
+ </div>
87
+ </template>
@@ -0,0 +1,61 @@
1
+ <script lang="ts" setup>
2
+ import type { UnsubscribeLinkBlock } 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: UnsubscribeLinkBlock
9
+ }>()
10
+
11
+ const appContext = useAppContextStore()
12
+ const emailEditorStore = useEmailEditorStore()
13
+
14
+ const editor = useEditor({
15
+ content: props.block.settings.content,
16
+ editable: false,
17
+ extensions: getTipTapExtensions({
18
+ preset: 'email',
19
+ dynamicValues: true,
20
+ }),
21
+ })
22
+
23
+ watch(
24
+ () => props.block.settings.content,
25
+ (newContent) => {
26
+ if (editor.value && newContent) {
27
+ editor.value.commands.setContent(newContent, { emitUpdate: false })
28
+ }
29
+ },
30
+ )
31
+
32
+ defineOptions({
33
+ name: 'UnsubscribeLinkBlock',
34
+ })
35
+ </script>
36
+
37
+ <template>
38
+ <div
39
+ class="rounded p-2 relative"
40
+ :style="{ backgroundColor: block.settings.backgroundColor }"
41
+ :data-testid="unsubscribeLinkBlockTestIds.container"
42
+ >
43
+ <div
44
+ :style="{
45
+ fontFamily: block.settings.fontFamily ?? emailEditorStore.resolvedSettings.fontFamily,
46
+ fontSize: `${block.settings.fontSize}px`,
47
+ color: block.settings.textColor ?? emailEditorStore.resolvedSettings.textColor,
48
+ textAlign: block.settings.alignment,
49
+ }"
50
+ >
51
+ <EditorContent :editor="editor" :data-testid="unsubscribeLinkBlockTestIds.editor" />
52
+ <div
53
+ v-show="editor?.isEmpty"
54
+ class="absolute inset-0 w-full h-full flex items-center justify-center text-neutral-400 text-sm"
55
+ :data-testid="unsubscribeLinkBlockTestIds.placeholder"
56
+ >
57
+ {{ $t('editor.placeholder.insert_text') }}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </template>