@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
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Nuxt Minimal Starter
2
+
3
+ Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
4
+
5
+ ## Setup
6
+
7
+ Make sure to install dependencies:
8
+
9
+ ```bash
10
+ # npm
11
+ npm install
12
+
13
+ # pnpm
14
+ pnpm install
15
+
16
+ # yarn
17
+ yarn install
18
+
19
+ # bun
20
+ bun install
21
+ ```
22
+
23
+ ## Development Server
24
+
25
+ Start the development server on `http://localhost:3000`:
26
+
27
+ ```bash
28
+ # npm
29
+ npm run dev
30
+
31
+ # pnpm
32
+ pnpm dev
33
+
34
+ # yarn
35
+ yarn dev
36
+
37
+ # bun
38
+ bun run dev
39
+ ```
40
+
41
+ ## Production
42
+
43
+ Build the application for production:
44
+
45
+ ```bash
46
+ # npm
47
+ npm run build
48
+
49
+ # pnpm
50
+ pnpm build
51
+
52
+ # yarn
53
+ yarn build
54
+
55
+ # bun
56
+ bun run build
57
+ ```
58
+
59
+ Locally preview production build:
60
+
61
+ ```bash
62
+ # npm
63
+ npm run preview
64
+
65
+ # pnpm
66
+ pnpm preview
67
+
68
+ # yarn
69
+ yarn preview
70
+
71
+ # bun
72
+ bun run preview
73
+ ```
74
+
75
+ Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
package/app/api/ai.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type {
2
+ RefineTextResponse,
3
+ TranslateTextRequest,
4
+ TranslationResponse,
5
+ GenerateTextRequest,
6
+ GenerateTextResponse,
7
+ } from '@dev.smartpricing/message-composer-utils/types'
8
+ import { apiClient } from './client'
9
+
10
+ export const aiApi = {
11
+ refineText: (text: string) =>
12
+ apiClient<RefineTextResponse>('/ai-integration/refine-text', {
13
+ method: 'POST',
14
+ body: { text },
15
+ }),
16
+
17
+ translate: (params: TranslateTextRequest) =>
18
+ apiClient<TranslationResponse>('/ai-integration/translate', {
19
+ method: 'POST',
20
+ body: params,
21
+ }),
22
+
23
+ generateText: (params: GenerateTextRequest) =>
24
+ apiClient<GenerateTextResponse>('/ai-integration/generate-text', {
25
+ method: 'POST',
26
+ body: params,
27
+ }),
28
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ BrandFromDb,
3
+ BrandProperty,
4
+ BrandCreateBody,
5
+ BrandUpdateBody,
6
+ Property,
7
+ PostEntity,
8
+ EmailMessage,
9
+ CompileEmailResponse,
10
+ } from '@dev.smartpricing/message-composer-utils/types'
11
+ import type { BrandCascadeData } from '@dev.smartpricing/message-composer-utils/utils'
12
+ import { apiClient } from './client'
13
+
14
+ export type BrandWithProperties = BrandFromDb & { properties: BrandProperty[] }
15
+
16
+ export const brandsApi = {
17
+ getAll: (options?: { sort_by?: 'created_at' | 'name'; direction?: 'asc' | 'desc' }) => {
18
+ const params = new URLSearchParams()
19
+ if (options?.sort_by) params.set('sort_by', options.sort_by)
20
+ if (options?.direction) params.set('direction', options.direction)
21
+ const query = params.size ? `?${params.toString()}` : ''
22
+ return apiClient<BrandFromDb[]>(`/brands${query}`)
23
+ },
24
+
25
+ getById: (id: string) => apiClient<BrandWithProperties>(`/brands/${id}`),
26
+
27
+ create: (data: BrandCreateBody) =>
28
+ apiClient<BrandFromDb>('/brands', {
29
+ method: 'POST',
30
+ body: data,
31
+ }),
32
+
33
+ update: (id: string, data: BrandUpdateBody) =>
34
+ apiClient<BrandFromDb>(`/brands/${id}`, {
35
+ method: 'PUT',
36
+ body: data,
37
+ }),
38
+
39
+ delete: (id: string) => apiClient(`/brands/${id}`, { method: 'DELETE' }),
40
+
41
+ preview: (data: {
42
+ message: PostEntity<EmailMessage>
43
+ parameters?: Record<string, unknown>
44
+ brand?: BrandCascadeData | null
45
+ }) =>
46
+ apiClient<CompileEmailResponse>('/message-groups/preview', {
47
+ method: 'POST',
48
+ body: data,
49
+ }),
50
+ }
51
+
52
+ export const propertiesApi = {
53
+ getAll: () => apiClient<Property[]>('/properties'),
54
+ }
@@ -0,0 +1,56 @@
1
+ import { ofetch } from 'ofetch'
2
+ import type { AppError } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { useAppContextStore } from '@/stores/appContext'
4
+
5
+ function assertAppError(data: unknown): data is AppError {
6
+ if (typeof data !== 'object' || data === null) {
7
+ return false
8
+ }
9
+ if ('code' in data && 'error' in data && 'statusCode' in data) {
10
+ return true
11
+ }
12
+ return false
13
+ }
14
+
15
+ export const apiClient = ofetch.create({
16
+ baseURL: useRuntimeConfig().public.COMPOSER_API_URL,
17
+ onRequest({ options }) {
18
+ const store = useAppContextStore()
19
+
20
+ if (store.authenticationToken) {
21
+ options.headers.set('Authorization', `Bearer ${store.authenticationToken}`)
22
+ }
23
+ if (store.owner_id) {
24
+ options.headers.set('sm-owner', store.owner_id)
25
+ }
26
+ if (store.metaWabaId) {
27
+ options.headers.set('x-meta-waba-id', store.metaWabaId)
28
+ }
29
+ if (store.product) {
30
+ options.headers.set('sm-product', store.product)
31
+ }
32
+ if (store.default_tags && store.default_tags.length > 0) {
33
+ options.headers.set('sm-default-tags', store.default_tags.join(','))
34
+ }
35
+ },
36
+ async onResponseError({ response }) {
37
+ if (response.status === 401) {
38
+ await useNuxtApp().callHook('composer:auth-error', { statusCode: 401 })
39
+ }
40
+
41
+ const error = response._data
42
+ if (assertAppError(error)) {
43
+ useToast().add({
44
+ color: 'error',
45
+ title: error.code,
46
+ description: error.message,
47
+ })
48
+ } else {
49
+ useToast().add({
50
+ color: 'error',
51
+ title: 'Error',
52
+ description: 'An error occurred',
53
+ })
54
+ }
55
+ },
56
+ })
@@ -0,0 +1,34 @@
1
+ import type {
2
+ TestEmailRequest,
3
+ CompileAndSendResponse,
4
+ GetMessageParametersResponse,
5
+ CompileMessageRequest,
6
+ CompileEmailResponse,
7
+ PreviewMessageRequest,
8
+ } from '@dev.smartpricing/message-composer-utils/types'
9
+ import { apiClient } from './client'
10
+
11
+ export const compilationApi = {
12
+ sendTestEmail: (data: TestEmailRequest) =>
13
+ apiClient<CompileAndSendResponse>('/message-groups/test-email', {
14
+ method: 'POST',
15
+ body: data,
16
+ }),
17
+
18
+ getParameters: (messageGroupId: string, language: string) =>
19
+ apiClient<GetMessageParametersResponse>(
20
+ `/message-groups/${messageGroupId}/parameters?language=${language}`,
21
+ ),
22
+
23
+ compileEmail: (messageGroupId: string, data: CompileMessageRequest) =>
24
+ apiClient<CompileEmailResponse>(`/message-groups/${messageGroupId}/compile`, {
25
+ method: 'POST',
26
+ body: data,
27
+ }),
28
+
29
+ previewEmail: (data: PreviewMessageRequest) =>
30
+ apiClient<CompileEmailResponse>('/message-groups/preview', {
31
+ method: 'POST',
32
+ body: data,
33
+ }),
34
+ }
@@ -0,0 +1,11 @@
1
+ import type { DynamicValuesResponse } from '@dev.smartpricing/message-composer-utils/types'
2
+ import { apiClient } from './client'
3
+
4
+ export const dynamicValuesApi = {
5
+ get: (product: string, language: string) => {
6
+ const params = new URLSearchParams()
7
+ params.set('product', product)
8
+ params.set('language', language)
9
+ return apiClient<DynamicValuesResponse>(`/dynamic-values?${params.toString()}`)
10
+ },
11
+ }
@@ -0,0 +1,11 @@
1
+ export * from './client'
2
+ export * from './languages'
3
+ export * from './messageGroups'
4
+ export * from './messages'
5
+ export * from './media'
6
+ export * from './ai'
7
+ export * from './meta'
8
+ export * from './compilation'
9
+ export * from './tracking'
10
+ export * from './dynamicValues'
11
+ export * from './brands'
@@ -0,0 +1,6 @@
1
+ import type { LanguageFromDb } from '@dev.smartpricing/message-composer-utils/types'
2
+ import { apiClient } from './client'
3
+
4
+ export const languagesApi = {
5
+ getAll: () => apiClient<LanguageFromDb[]>('/languages'),
6
+ }
@@ -0,0 +1,26 @@
1
+ import type { MediaFromDb } from '@dev.smartpricing/message-composer-utils/types'
2
+ import { apiClient } from './client'
3
+
4
+ export const mediaApi = {
5
+ list: () => apiClient<MediaFromDb[]>('/media'),
6
+
7
+ upload: (file: File) => {
8
+ const formData = new FormData()
9
+ formData.append('file', file)
10
+ formData.append('fileName', file.name)
11
+ return apiClient<MediaFromDb>('/media/upload', {
12
+ method: 'POST',
13
+ body: formData,
14
+ })
15
+ },
16
+
17
+ uploadToSmartchat: (id: string) =>
18
+ apiClient<MediaFromDb>(`/media/${id}/upload-to-smartchat`, {
19
+ method: 'POST',
20
+ }),
21
+
22
+ delete: (id: string) =>
23
+ apiClient(`/media/${id}`, {
24
+ method: 'DELETE',
25
+ }),
26
+ }
@@ -0,0 +1,49 @@
1
+ import type {
2
+ MessageGroupFromDb,
3
+ MessageGroupWithMessages,
4
+ MessageGroupWithMessageSummary,
5
+ MessageGroupValidationResponse,
6
+ PostEntity,
7
+ MessageGroup,
8
+ RenderType,
9
+ } from '@dev.smartpricing/message-composer-utils/types'
10
+ import { apiClient } from './client'
11
+
12
+ export const messageGroupsApi = {
13
+ getAll: (options?: { renderType?: RenderType; tags?: string[]; tagsMode?: 'and' | 'or' }) => {
14
+ const params = new URLSearchParams()
15
+ if (options?.renderType) params.set('render_type', options.renderType)
16
+ if (options?.tags && options.tags.length > 0) {
17
+ options.tags.forEach((tag) => params.append('tags', tag))
18
+ }
19
+ if (options?.tagsMode) params.set('tags_mode', options.tagsMode)
20
+
21
+ const query = params.size ? `?${params.toString()}` : ''
22
+ return apiClient<MessageGroupWithMessageSummary[]>(`/message-groups${query}`)
23
+ },
24
+
25
+ getById: (id: string) => apiClient<MessageGroupWithMessages>(`/message-groups/${id}`),
26
+
27
+ create: (data: PostEntity<MessageGroup>) =>
28
+ apiClient<MessageGroupFromDb>('/message-groups', {
29
+ method: 'POST',
30
+ body: data,
31
+ }),
32
+
33
+ update: (id: string, data: Partial<PostEntity<MessageGroup>>) =>
34
+ apiClient<MessageGroupFromDb>(`/message-groups/${id}`, {
35
+ method: 'PUT',
36
+ body: data,
37
+ }),
38
+
39
+ delete: (id: string) => apiClient(`/message-groups/${id}`, { method: 'DELETE' }),
40
+
41
+ publish: (id: string) =>
42
+ apiClient<MessageGroupFromDb>(`/message-groups/${id}/publish`, { method: 'PUT' }),
43
+
44
+ unpublish: (id: string) =>
45
+ apiClient<MessageGroupFromDb>(`/message-groups/${id}/unpublish`, { method: 'PUT' }),
46
+
47
+ validate: (id: string) =>
48
+ apiClient<MessageGroupValidationResponse>(`/message-groups/${id}/validate`),
49
+ }
@@ -0,0 +1,22 @@
1
+ import type {
2
+ Message,
3
+ MessageFromDb,
4
+ PostEntity,
5
+ } from '@dev.smartpricing/message-composer-utils/types'
6
+ import { apiClient } from './client'
7
+
8
+ export const messagesApi = {
9
+ create: (data: PostEntity<Message>) =>
10
+ apiClient<MessageFromDb>('/messages', {
11
+ method: 'POST',
12
+ body: data,
13
+ }),
14
+
15
+ update: (id: string, data: Partial<PostEntity<Message>>) =>
16
+ apiClient<MessageFromDb>(`/messages/${id}`, {
17
+ method: 'PUT',
18
+ body: data,
19
+ }),
20
+
21
+ delete: (id: string) => apiClient(`/messages/${id}`, { method: 'DELETE' }),
22
+ }
@@ -0,0 +1,18 @@
1
+ import type {
2
+ SubmissionJob,
3
+ SubmissionJobResponse,
4
+ } from '@dev.smartpricing/message-composer-utils/types'
5
+ import { apiClient } from './client'
6
+
7
+ export const metaApi = {
8
+ submitGroup: (messageGroupId: string) =>
9
+ apiClient<SubmissionJobResponse>(`/integrations/meta/submit-group/${messageGroupId}`, {
10
+ method: 'POST',
11
+ body: {},
12
+ }),
13
+
14
+ getJobStatus: (jobId: string) =>
15
+ apiClient<SubmissionJob>(`/integrations/meta/submission-jobs/${jobId}`, {
16
+ method: 'GET',
17
+ }),
18
+ }
@@ -0,0 +1,9 @@
1
+ import { apiClient } from './client'
2
+
3
+ export const trackingApi = {
4
+ event: (eventName: string, properties?: Record<string, unknown>) =>
5
+ apiClient<{ success: boolean }>('/tracking/event', {
6
+ method: 'POST',
7
+ body: { eventName, properties },
8
+ }),
9
+ }
@@ -0,0 +1,281 @@
1
+ <script setup lang="ts">
2
+ import type { NavigationMenuItem } from '#ui/types'
3
+ import type {
4
+ BrandFromDb,
5
+ BrandDefaultEmailSettings,
6
+ BrandDefaultButtonSettings,
7
+ BrandCreateBody,
8
+ BrandSocials,
9
+ } from '@dev.smartpricing/message-composer-utils/types'
10
+ import { isValidUrl } from '@dev.smartpricing/message-composer-utils/utils'
11
+ import type { BrandCascadeData } from '@dev.smartpricing/message-composer-utils/utils'
12
+ import type { BrandWithProperties } from '~/api/brands'
13
+ import BrandPreview from '~/components/Brand/BrandPreview.vue'
14
+ import BrandStepGeneral from '~/components/Brand/Steps/BrandStepGeneral.vue'
15
+ import BrandStepVisual from '~/components/Brand/Steps/BrandStepVisual.vue'
16
+ import BrandStepSocials from '~/components/Brand/Steps/BrandStepSocials.vue'
17
+ import { useBrandQuery, useCreateBrandMutation, useUpdateBrandMutation } from '~/queries'
18
+
19
+ defineOptions({ name: 'BrandDrawer' })
20
+
21
+ const isOpen = defineModel<boolean>({ required: true })
22
+
23
+ const props = defineProps<{
24
+ brandId?: string
25
+ }>()
26
+
27
+ const emit = defineEmits<{
28
+ saved: [brand: BrandFromDb]
29
+ }>()
30
+
31
+ const { t } = useI18n()
32
+
33
+ const isEditMode = computed(() => !!props.brandId)
34
+
35
+ // Queries
36
+ const { data: existingBrand, isPending: isBrandLoading } = useBrandQuery(
37
+ computed(() => props.brandId ?? ''),
38
+ )
39
+
40
+ // Mutations
41
+ const { mutateAsync: createBrand, isLoading: isCreating } = useCreateBrandMutation()
42
+ const { mutateAsync: updateBrand, isLoading: isUpdating } = useUpdateBrandMutation()
43
+
44
+ const isSaving = computed(() => isCreating.value || isUpdating.value)
45
+
46
+ // ============================================================================
47
+ // NAVIGATION
48
+ // ============================================================================
49
+ type Step = 'general' | 'visual' | 'socials'
50
+ const activeStep = ref<Step>('general')
51
+
52
+ const navItems = computed<NavigationMenuItem[]>(() => [
53
+ {
54
+ label: t('pages.brands.steps.general'),
55
+ icon: 'ph:info',
56
+ active: activeStep.value === 'general',
57
+ onSelect: () => {
58
+ activeStep.value = 'general'
59
+ },
60
+ },
61
+ {
62
+ label: t('pages.brands.steps.visual_style'),
63
+ icon: 'ph:paint-brush',
64
+ active: activeStep.value === 'visual',
65
+ onSelect: () => {
66
+ activeStep.value = 'visual'
67
+ },
68
+ },
69
+ {
70
+ label: t('pages.brands.steps.socials'),
71
+ icon: 'ph:share-network',
72
+ active: activeStep.value === 'socials',
73
+ onSelect: () => {
74
+ activeStep.value = 'socials'
75
+ },
76
+ },
77
+ ])
78
+
79
+ // ============================================================================
80
+ // FORM STATE
81
+ // ============================================================================
82
+ const name = ref('')
83
+ const selectedPropertyIds = ref<string[]>([])
84
+ const logoMediaId = ref<string | null>(null)
85
+ const logoUrl = ref<string>('')
86
+ const colors = ref<string[]>([])
87
+
88
+ const defaultEmailSettings = ref<BrandDefaultEmailSettings>({
89
+ backgroundColor: undefined,
90
+ contentBackgroundColor: undefined,
91
+ textColor: undefined,
92
+ fontFamily: undefined,
93
+ })
94
+
95
+ const defaultButtonSettings = ref<BrandDefaultButtonSettings>({
96
+ textColor: '#FFFFFF',
97
+ backgroundColor: '#000000',
98
+ })
99
+
100
+ const socials = ref<BrandSocials>({})
101
+
102
+ // ============================================================================
103
+ // POPULATE / RESET
104
+ // ============================================================================
105
+ watch(
106
+ [() => props.brandId, existingBrand],
107
+ ([id, brand]) => {
108
+ if (!id || !brand) {
109
+ resetForm()
110
+ return
111
+ }
112
+ populateForm(brand)
113
+ },
114
+ { immediate: true },
115
+ )
116
+
117
+ function populateForm(brand: BrandWithProperties) {
118
+ name.value = brand.name
119
+ selectedPropertyIds.value = brand.properties?.map((bp) => bp.property_id) ?? []
120
+ logoMediaId.value = brand.logo_media_id ?? null
121
+ logoUrl.value = brand.logo_url ?? ''
122
+ colors.value = [...brand.colors]
123
+ defaultEmailSettings.value = { ...brand.default_email_settings }
124
+ defaultButtonSettings.value = { ...brand.default_button_settings }
125
+ socials.value = brand.default_socials ? { ...brand.default_socials } : {}
126
+ }
127
+
128
+ function resetForm() {
129
+ name.value = ''
130
+ selectedPropertyIds.value = []
131
+ logoMediaId.value = null
132
+ logoUrl.value = ''
133
+ colors.value = []
134
+ defaultEmailSettings.value = {
135
+ backgroundColor: '#F3F4F6',
136
+ contentBackgroundColor: '#FFFFFF',
137
+ textColor: '#000000',
138
+ fontFamily: 'Arial',
139
+ }
140
+ defaultButtonSettings.value = {
141
+ textColor: '#FFFFFF',
142
+ backgroundColor: '#000000',
143
+ }
144
+ socials.value = {}
145
+ activeStep.value = 'general'
146
+ }
147
+
148
+ watch(isOpen, (val) => {
149
+ if (!val) resetForm()
150
+ })
151
+
152
+ // ============================================================================
153
+ // PREVIEW DATA
154
+ // ============================================================================
155
+ const brandCascadeData = computed<BrandCascadeData>(() => ({
156
+ default_email_settings: defaultEmailSettings.value,
157
+ default_button_settings: defaultButtonSettings.value,
158
+ default_socials: socials.value,
159
+ logo_url: logoUrl.value || null,
160
+ }))
161
+
162
+ // ============================================================================
163
+ // SAVE / DELETE
164
+ // ============================================================================
165
+ function hasInvalidSocialUrls(): boolean {
166
+ return Object.values(socials.value).some((url) => !!url && !isValidUrl(url))
167
+ }
168
+
169
+ async function handleSave() {
170
+ if (!name.value.trim()) {
171
+ useToast().add({
172
+ color: 'error',
173
+ title: t('pages.brands.validation.name_required'),
174
+ })
175
+ activeStep.value = 'general'
176
+ return
177
+ }
178
+
179
+ if (hasInvalidSocialUrls()) {
180
+ useToast().add({
181
+ color: 'error',
182
+ title: t('pages.brands.validation.invalid_social_url'),
183
+ })
184
+ activeStep.value = 'socials'
185
+ return
186
+ }
187
+
188
+ const payload: BrandCreateBody = {
189
+ name: name.value.trim(),
190
+ logo_media_id: logoMediaId.value,
191
+ colors: colors.value,
192
+ default_email_settings: defaultEmailSettings.value,
193
+ default_button_settings: defaultButtonSettings.value,
194
+ default_socials: socials.value,
195
+ property_ids: selectedPropertyIds.value,
196
+ }
197
+
198
+ try {
199
+ let result: BrandFromDb
200
+ if (isEditMode.value && props.brandId) {
201
+ result = await updateBrand({ id: props.brandId, data: payload })
202
+ } else {
203
+ result = await createBrand(payload)
204
+ }
205
+
206
+ useToast().add({
207
+ color: 'success',
208
+ title: t('common.success'),
209
+ description: isEditMode.value
210
+ ? t('pages.brands.brand_updated')
211
+ : t('pages.brands.brand_created'),
212
+ })
213
+ emit('saved', result)
214
+ } catch {
215
+ useToast().add({
216
+ color: 'error',
217
+ title: t('common.error'),
218
+ description: t('pages.brands.save_error'),
219
+ })
220
+ }
221
+ }
222
+ </script>
223
+
224
+ <template>
225
+ <USlideover
226
+ v-model:open="isOpen"
227
+ :title="isEditMode ? t('pages.brands.edit_brand') : t('pages.brands.new_brand')"
228
+ close-icon="ph:x"
229
+ side="right"
230
+ :ui="{ content: 'max-w-7xl', body: 'sm:p-0 p-0', footer: 'justify-end' }"
231
+ >
232
+ <template #body>
233
+ <!-- Loading state -->
234
+ <div v-if="isEditMode && isBrandLoading" class="flex items-center justify-center h-full p-8">
235
+ <UIcon name="ph:spinner" class="animate-spin h-8 w-8 text-neutral-400" />
236
+ </div>
237
+
238
+ <div v-else class="grid grid-cols-[200px_1fr_400px] divide-x divide-muted min-h-full">
239
+ <!-- Left: Navigation -->
240
+ <div class="p-4">
241
+ <UNavigationMenu color="secondary" orientation="vertical" :items="navItems" />
242
+ </div>
243
+
244
+ <!-- Center: Form content -->
245
+ <div class="overflow-y-auto p-6 space-y-6">
246
+ <BrandStepGeneral
247
+ v-if="activeStep === 'general'"
248
+ v-model:name="name"
249
+ v-model:selected-property-ids="selectedPropertyIds"
250
+ />
251
+
252
+ <BrandStepVisual
253
+ v-if="activeStep === 'visual'"
254
+ v-model:logo-media-id="logoMediaId"
255
+ v-model:logo-url="logoUrl"
256
+ v-model:colors="colors"
257
+ v-model:default-email-settings="defaultEmailSettings"
258
+ v-model:default-button-settings="defaultButtonSettings"
259
+ />
260
+
261
+ <BrandStepSocials v-if="activeStep === 'socials'" v-model="socials" />
262
+ </div>
263
+
264
+ <!-- Right: Live preview -->
265
+ <div class="bg-neutral-50 overflow-y-auto">
266
+ <BrandPreview :brand="brandCascadeData" />
267
+ </div>
268
+ </div>
269
+ </template>
270
+
271
+ <template #footer>
272
+ <UButton
273
+ color="neutral"
274
+ variant="outline"
275
+ :label="t('common.actions.cancel')"
276
+ @click="isOpen = false"
277
+ />
278
+ <UButton :label="t('common.actions.save')" :loading="isSaving" @click="handleSave" />
279
+ </template>
280
+ </USlideover>
281
+ </template>