From 250558c2f6617432ceaffb73abe3bb209afb9431 Mon Sep 17 00:00:00 2001 From: Muhsin Date: Wed, 29 Oct 2025 19:25:17 +0530 Subject: [PATCH] feat: add base components --- .../template-preview/README.md | 121 +++ .../TemplatePreview.story.vue | 322 +++++++ .../template-preview/TemplatePreview.vue | 113 +++ .../TemplatePreviewExamples.story.vue | 792 ++++++++++++++++++ .../components-next/template-preview/index.js | 6 + .../dashboard/services/TemplateNormalizer.js | 153 ++++ .../services/TemplateTypeDetector.js | 71 ++ 7 files changed, 1578 insertions(+) create mode 100644 app/javascript/dashboard/components-next/template-preview/README.md create mode 100644 app/javascript/dashboard/components-next/template-preview/TemplatePreview.story.vue create mode 100644 app/javascript/dashboard/components-next/template-preview/TemplatePreview.vue create mode 100644 app/javascript/dashboard/components-next/template-preview/TemplatePreviewExamples.story.vue create mode 100644 app/javascript/dashboard/components-next/template-preview/index.js create mode 100644 app/javascript/dashboard/services/TemplateNormalizer.js create mode 100644 app/javascript/dashboard/services/TemplateTypeDetector.js diff --git a/app/javascript/dashboard/components-next/template-preview/README.md b/app/javascript/dashboard/components-next/template-preview/README.md new file mode 100644 index 000000000..38aadc1ef --- /dev/null +++ b/app/javascript/dashboard/components-next/template-preview/README.md @@ -0,0 +1,121 @@ +# Template Preview System + +A comprehensive template preview system for WhatsApp and Twilio templates that provides real-time visual previews during template selection, campaign creation, and displays accurate rendered templates in message threads. + +## Features + +- **Multi-Platform Support**: WhatsApp and Twilio templates +- **Real-Time Preview**: Interactive variable editing with live preview updates +- **Template Type Detection**: Automatic detection and routing to appropriate preview components +- **Variable Validation**: Real-time validation with error states +- **Responsive Design**: Works across mobile and desktop +- **Storybook Integration**: Complete component documentation and examples + +## Supported Template Types + +### WhatsApp Templates +- **Text Templates**: Simple text with variables +- **Media Templates**: Image/Video/Document headers with text +- **Interactive Templates**: URL, Phone, Quick Reply buttons +- **Copy Code Templates**: Discount/promo code functionality +- **Combined Templates**: Media + Text + Buttons + +### Twilio Templates +- **Text Templates**: Simple text with variables +- **Media Templates**: Text + Image/Video +- **Quick Reply Templates**: Interactive action buttons + +## Usage + +### Basic Usage + +```vue + + + +``` + +### Preview Modes + +- **`display`**: Static preview with populated variables (for sent messages) +- **`interactive`**: Live editing with input fields (for campaign creation) +- **`placeholder`**: Show example values from template definition + +### Integration Examples + +See the stories in `stories/TemplatePreview.stories.js` for comprehensive examples covering all template types and use cases. + +## Component Architecture + +``` +template-preview/ +├── TemplatePreview.vue # Main container component +├── components/ # Shared UI components +│ ├── MessageBubble.vue # Platform-specific message bubble +│ └── VariableInput.vue # Variable input field +├── whatsapp/ # WhatsApp-specific previews +│ ├── WhatsAppTextPreview.vue +│ ├── WhatsAppMediaPreview.vue +│ ├── WhatsAppButtonPreview.vue +│ └── WhatsAppCopyCodePreview.vue +├── twilio/ # Twilio-specific previews +│ ├── TwilioTextPreview.vue +│ ├── TwilioMediaPreview.vue +│ └── TwilioQuickReplyPreview.vue +└── stories/ # Storybook documentation + └── TemplatePreview.stories.js +``` + +## Services & Composables + +- **`TemplateTypeDetector`**: Identifies template types across platforms +- **`TemplateNormalizer`**: Converts platform-specific formats to unified structure +- **`useTemplateVariables`**: Composable for variable management and validation + +## Development + +### Running Storybook + +```bash +npm run storybook +``` + +### Adding New Template Types + +1. Add type detection logic in `TemplateTypeDetector.js` +2. Add normalization logic in `TemplateNormalizer.js` +3. Create preview component in appropriate platform folder +4. Add component mapping in `TemplatePreview.vue` +5. Add story examples in `TemplatePreview.stories.js` + +## Design System + +The system uses Chatwoot's existing design tokens and follows the established component patterns: + +- **Colors**: Uses `n-alpha-2`, `n-slate-12`, etc. from the design system +- **Spacing**: Consistent with existing message bubble components +- **Typography**: Matches WhatsApp/platform styling +- **Buttons**: Uses existing `Button.vue` component + +## Testing + +The system includes comprehensive Storybook stories that serve as both documentation and visual regression testing. Each template type and mode combination is covered. \ No newline at end of file diff --git a/app/javascript/dashboard/components-next/template-preview/TemplatePreview.story.vue b/app/javascript/dashboard/components-next/template-preview/TemplatePreview.story.vue new file mode 100644 index 000000000..3694071d7 --- /dev/null +++ b/app/javascript/dashboard/components-next/template-preview/TemplatePreview.story.vue @@ -0,0 +1,322 @@ + + + diff --git a/app/javascript/dashboard/components-next/template-preview/TemplatePreview.vue b/app/javascript/dashboard/components-next/template-preview/TemplatePreview.vue new file mode 100644 index 000000000..aa1479bac --- /dev/null +++ b/app/javascript/dashboard/components-next/template-preview/TemplatePreview.vue @@ -0,0 +1,113 @@ + + + diff --git a/app/javascript/dashboard/components-next/template-preview/TemplatePreviewExamples.story.vue b/app/javascript/dashboard/components-next/template-preview/TemplatePreviewExamples.story.vue new file mode 100644 index 000000000..972c3e4f0 --- /dev/null +++ b/app/javascript/dashboard/components-next/template-preview/TemplatePreviewExamples.story.vue @@ -0,0 +1,792 @@ + + + diff --git a/app/javascript/dashboard/components-next/template-preview/index.js b/app/javascript/dashboard/components-next/template-preview/index.js new file mode 100644 index 000000000..b7f1bd6b0 --- /dev/null +++ b/app/javascript/dashboard/components-next/template-preview/index.js @@ -0,0 +1,6 @@ +// Main Template Preview Component +export { default as TemplatePreview } from './TemplatePreview.vue'; + +// Core Services +export { TemplateTypeDetector } from 'dashboard/services/TemplateTypeDetector'; +export { TemplateNormalizer } from 'dashboard/services/TemplateNormalizer'; diff --git a/app/javascript/dashboard/services/TemplateNormalizer.js b/app/javascript/dashboard/services/TemplateNormalizer.js new file mode 100644 index 000000000..7ff69a44e --- /dev/null +++ b/app/javascript/dashboard/services/TemplateNormalizer.js @@ -0,0 +1,153 @@ +import { TemplateTypeDetector } from './TemplateTypeDetector'; + +/** + * TemplateNormalizer - Convert platform-specific formats to unified structure + */ +export class TemplateNormalizer { + /** + * Normalize WhatsApp template to unified format + * @param {Object} template - WhatsApp template object + * @returns {Object} - Normalized template + */ + static normalizeWhatsApp(template) { + const components = template.components || []; + + return { + id: template.id, + name: template.name, + platform: 'whatsapp', + type: TemplateTypeDetector.detectWhatsAppType(template), + parameterFormat: template.parameter_format || 'POSITIONAL', + header: components.find(c => c.type === 'HEADER'), + body: components.find(c => c.type === 'BODY'), + footer: components.find(c => c.type === 'FOOTER'), + buttons: components.find(c => c.type === 'BUTTONS')?.buttons || [], + variables: this.extractWhatsAppVariables(template), + category: template.category, + language: template.language, + originalTemplate: template, + }; + } + + /** + * Normalize Twilio template to unified format + * @param {Object} template - Twilio template object + * @returns {Object} - Normalized template + */ + static normalizeTwilio(template) { + const typeData = template.types?.[`twilio/${template.template_type}`] || {}; + + return { + contentSid: template.content_sid, + name: template.friendly_name, + platform: 'twilio', + type: TemplateTypeDetector.detectTwilioType(template), + body: template.body || typeData.body, + media: typeData.media || [], + actions: typeData.actions || [], + variables: template.variables || {}, + category: template.category || 'utility', + language: template.language || 'en', + originalTemplate: template, + }; + } + + /** + * Extract variables from WhatsApp template + * @param {Object} template - WhatsApp template object + * @returns {Object} - Variables object with example values + */ + static extractWhatsAppVariables(template) { + const variables = {}; + const components = template.components || []; + + components.forEach(component => { + // Extract from text content + if (component.text) { + const matches = component.text.match(/\{\{([^}]+)\}\}/g) || []; + matches.forEach(match => { + const variable = match.replace(/[{}]/g, ''); + + if (template.parameter_format === 'NAMED') { + // Named parameters: {{customer_name}} + const example = + component.example?.body_text_named_params?.find( + p => p.param_name === variable + )?.example || ''; + variables[variable] = example; + } else { + // Positional parameters: {{1}}, {{2}} + const position = parseInt(variable, 10) - 1; + const example = component.example?.body_text?.[0]?.[position] || ''; + variables[variable] = example; + } + }); + } + + // Extract from button URLs + if (component.buttons) { + component.buttons.forEach(button => { + if (button.url) { + const matches = button.url.match(/\{\{([^}]+)\}\}/g) || []; + matches.forEach(match => { + const variable = match.replace(/[{}]/g, ''); + const example = button.example?.[0] || ''; + variables[variable] = example; + }); + } + }); + } + }); + + return variables; + } + + /** + * Extract variables from Twilio template + * @param {Object} template - Twilio template object + * @returns {Object} - Variables object with example values + */ + static extractTwilioVariables(template) { + const variables = {}; + + // Extract from body text + if (template.body) { + const matches = template.body.match(/\{\{([^}]+)\}\}/g) || []; + matches.forEach(match => { + const variable = match.replace(/[{}]/g, ''); + variables[variable] = template.variables?.[variable] || ''; + }); + } + + // Extract from media URLs + const typeData = template.types?.[`twilio/${template.template_type}`] || {}; + if (typeData.media) { + typeData.media.forEach(mediaUrl => { + const matches = mediaUrl.match(/\{\{([^}]+)\}\}/g) || []; + matches.forEach(match => { + const variable = match.replace(/[{}]/g, ''); + variables[variable] = template.variables?.[variable] || ''; + }); + }); + } + + return variables; + } + + /** + * Normalize template from any platform + * @param {Object} template - Template object + * @param {string} platform - Platform identifier ('whatsapp' | 'twilio') + * @returns {Object} - Normalized template + */ + static normalize(template, platform) { + switch (platform) { + case 'whatsapp': + return this.normalizeWhatsApp(template); + case 'twilio': + return this.normalizeTwilio(template); + default: + throw new Error(`Unsupported platform: ${platform}`); + } + } +} diff --git a/app/javascript/dashboard/services/TemplateTypeDetector.js b/app/javascript/dashboard/services/TemplateTypeDetector.js new file mode 100644 index 000000000..33948affe --- /dev/null +++ b/app/javascript/dashboard/services/TemplateTypeDetector.js @@ -0,0 +1,71 @@ +/** + * TemplateTypeDetector - Unified service to identify template types across platforms + */ +export class TemplateTypeDetector { + /** + * Detect WhatsApp template type based on components + * @param {Object} template - WhatsApp template object + * @returns {string} - Template type identifier + */ + static detectWhatsAppType(template) { + const components = template.components || []; + const hasHeader = components.find(c => c.type === 'HEADER'); + const hasButtons = components.find(c => c.type === 'BUTTONS'); + + // Media templates (priority: header media) + if (hasHeader?.format === 'IMAGE') return 'whatsapp-media-image'; + if (hasHeader?.format === 'VIDEO') return 'whatsapp-media-video'; + if (hasHeader?.format === 'DOCUMENT') return 'whatsapp-media-document'; + + // Interactive templates (priority: special buttons) + if (hasButtons) { + const copyCodeButton = hasButtons.buttons?.find( + b => b.type === 'COPY_CODE' + ); + if (copyCodeButton) return 'whatsapp-copy-code'; + return 'whatsapp-interactive'; + } + + // Text templates + if (hasHeader?.format === 'TEXT') return 'whatsapp-text-header'; + return 'whatsapp-text'; + } + + /** + * Detect Twilio template type + * @param {Object} template - Twilio template object + * @returns {string} - Template type identifier + */ + static detectTwilioType(template) { + switch (template.template_type) { + case 'media': + return 'twilio-media'; + case 'quick_reply': + return 'twilio-quick-reply'; + default: + return 'twilio-text'; + } + } + + /** + * Get all supported template types + * @returns {Array} - Array of supported template types + */ + static getSupportedTypes() { + return [ + // WhatsApp types + 'whatsapp-text', + 'whatsapp-text-header', + 'whatsapp-media-image', + 'whatsapp-media-video', + 'whatsapp-media-document', + 'whatsapp-interactive', + 'whatsapp-copy-code', + + // Twilio types + 'twilio-text', + 'twilio-media', + 'twilio-quick-reply', + ]; + } +}