UNPKG

@helping-desk/web-sdk

Version:

Web SDK for Helping Desk - Ticket creation and support widget integration

462 lines (431 loc) 13.4 kB
/** * Support Widget Component * * A floating support widget that provides ticket creation functionality */ export class ChatBubbleWidget { constructor(config, onCreateTicket) { this.container = null; this.bubble = null; this.widget = null; this.isOpen = false; this.stylesInjected = false; this.config = { position: config.position || 'bottom-right', primaryColor: config.primaryColor || '#667eea', bubbleIcon: config.bubbleIcon || '💬', }; this.onCreateTicket = onCreateTicket; } /** * Initialize and inject the widget into the page */ init() { if (this.container) { return; // Already initialized } this.injectStyles(); this.createWidget(); this.attachEventListeners(); } /** * Inject CSS styles into the page */ injectStyles() { if (this.stylesInjected) { return; } const styleId = 'helping-desk-widget-styles'; if (document.getElementById(styleId)) { this.stylesInjected = true; return; } const style = document.createElement('style'); style.id = styleId; style.textContent = this.getStyles(); document.head.appendChild(style); this.stylesInjected = true; } /** * Get CSS styles for the widget */ getStyles() { const primaryColor = this.config.primaryColor || '#667eea'; return ` .helping-desk-widget-container { position: fixed; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } .helping-desk-widget-container.bottom-right { bottom: 20px; right: 20px; } .helping-desk-widget-container.bottom-left { bottom: 20px; left: 20px; } .helping-desk-widget-container.top-right { top: 20px; right: 20px; } .helping-desk-widget-container.top-left { top: 20px; left: 20px; } .helping-desk-bubble { width: 60px; height: 60px; border-radius: 50%; background: ${primaryColor}; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 24px; transition: transform 0.2s, box-shadow 0.2s; position: relative; } .helping-desk-bubble:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); } .helping-desk-widget { position: absolute; bottom: 80px; right: 0; width: 380px; max-width: calc(100vw - 40px); height: 600px; max-height: calc(100vh - 100px); background: white; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); display: none; flex-direction: column; overflow: hidden; animation: slideUp 0.3s ease-out; } .helping-desk-widget-container.bottom-left .helping-desk-widget { right: auto; left: 0; } .helping-desk-widget-container.top-right .helping-desk-widget { bottom: auto; top: 80px; } .helping-desk-widget-container.top-left .helping-desk-widget { bottom: auto; top: 80px; right: auto; left: 0; } .helping-desk-widget.open { display: flex; } @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .helping-desk-widget-header { background: ${primaryColor}; color: white; padding: 20px; border-radius: 16px 16px 0 0; } .helping-desk-widget-title { font-size: 18px; font-weight: 600; margin: 0; } .helping-desk-widget-subtitle { font-size: 12px; opacity: 0.9; margin: 4px 0 0 0; } .helping-desk-widget-content { flex: 1; overflow-y: auto; padding: 20px; } .helping-desk-ticket-form { display: flex; flex-direction: column; gap: 16px; } .helping-desk-form-group { display: flex; flex-direction: column; gap: 8px; } .helping-desk-form-label { font-size: 14px; font-weight: 500; color: #333; } .helping-desk-form-input, .helping-desk-form-textarea, .helping-desk-form-select { padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; font-family: inherit; transition: border-color 0.2s; } .helping-desk-form-input:focus, .helping-desk-form-textarea:focus, .helping-desk-form-select:focus { outline: none; border-color: ${primaryColor}; } .helping-desk-form-textarea { resize: vertical; min-height: 120px; } .helping-desk-form-button { padding: 12px; background: ${primaryColor}; color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.2s; } .helping-desk-form-button:hover:not(:disabled) { background: ${primaryColor}dd; transform: translateY(-1px); } .helping-desk-form-button:disabled { opacity: 0.6; cursor: not-allowed; } .helping-desk-message { padding: 12px; border-radius: 8px; margin-bottom: 12px; font-size: 14px; } .helping-desk-message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .helping-desk-message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } @media (max-width: 480px) { .helping-desk-widget { width: calc(100vw - 20px); height: calc(100vh - 100px); border-radius: 16px 16px 0 0; bottom: 0; right: 10px; } .helping-desk-widget-container.bottom-left .helping-desk-widget { left: 10px; } } `; } /** * Create the widget DOM structure */ createWidget() { // Create container this.container = document.createElement('div'); this.container.className = `helping-desk-widget-container ${this.config.position || 'bottom-right'}`; // Create bubble button this.bubble = document.createElement('div'); this.bubble.className = 'helping-desk-bubble'; this.bubble.textContent = this.config.bubbleIcon || '💬'; this.bubble.setAttribute('aria-label', 'Open support widget'); // Create widget panel this.widget = document.createElement('div'); this.widget.className = 'helping-desk-widget'; this.widget.innerHTML = this.getWidgetHTML(); // Append to container this.container.appendChild(this.bubble); this.container.appendChild(this.widget); document.body.appendChild(this.container); } /** * Get widget HTML structure */ getWidgetHTML() { return ` <div class="helping-desk-widget-header"> <h3 class="helping-desk-widget-title">Need Help?</h3> <p class="helping-desk-widget-subtitle">We're here to assist you</p> </div> <div class="helping-desk-widget-content"> ${this.getTicketFormHTML()} </div> `; } /** * Get ticket form HTML */ getTicketFormHTML() { return ` <form class="helping-desk-ticket-form" id="helping-desk-ticket-form"> <div class="helping-desk-form-group"> <label class="helping-desk-form-label">Title *</label> <input type="text" class="helping-desk-form-input" name="title" placeholder="Brief description of your issue" required /> </div> <div class="helping-desk-form-group"> <label class="helping-desk-form-label">Description *</label> <textarea class="helping-desk-form-textarea" name="description" placeholder="Please provide detailed information about your issue..." required ></textarea> </div> <div class="helping-desk-form-group"> <label class="helping-desk-form-label">Priority</label> <select class="helping-desk-form-select" name="priority"> <option value="low">Low</option> <option value="medium" selected>Medium</option> <option value="high">High</option> <option value="urgent">Urgent</option> </select> </div> <div class="helping-desk-form-group"> <label class="helping-desk-form-label">Category</label> <select class="helping-desk-form-select" name="category"> <option value="general" selected>General</option> <option value="technical">Technical</option> <option value="billing">Billing</option> <option value="feature_request">Feature Request</option> <option value="bug_report">Bug Report</option> </select> </div> <button type="submit" class="helping-desk-form-button" id="helping-desk-submit-btn"> Submit Ticket </button> </form> `; } /** * Attach event listeners */ attachEventListeners() { // Bubble click if (this.bubble) { this.bubble.addEventListener('click', () => this.toggleWidget()); } // Form submission const form = this.widget?.querySelector('#helping-desk-ticket-form'); form?.addEventListener('submit', (e) => { e.preventDefault(); this.handleFormSubmit(form); }); // Close on outside click (optional - can be enabled) // document.addEventListener('click', (e) => { // if (this.isOpen && this.container && !this.container.contains(e.target as Node)) { // this.closeWidget(); // } // }); } /** * Toggle widget open/close */ toggleWidget() { if (this.isOpen) { this.closeWidget(); } else { this.openWidget(); } } /** * Open widget */ openWidget() { if (this.widget) { this.widget.classList.add('open'); this.isOpen = true; } } /** * Close widget */ closeWidget() { if (this.widget) { this.widget.classList.remove('open'); this.isOpen = false; } } /** * Handle form submission */ async handleFormSubmit(form) { const submitBtn = form.querySelector('#helping-desk-submit-btn'); const formData = new FormData(form); const title = formData.get('title'); const description = formData.get('description'); const priority = formData.get('priority'); const category = formData.get('category'); // Disable submit button submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; // Remove any existing messages const existingMessages = form.querySelectorAll('.helping-desk-message'); existingMessages.forEach(msg => msg.remove()); try { const ticket = await this.onCreateTicket({ title, description, priority, category }); // Show success message const successMsg = document.createElement('div'); successMsg.className = 'helping-desk-message success'; successMsg.textContent = `✅ Ticket created successfully! Ticket ID: ${ticket.id}`; form.insertBefore(successMsg, form.firstChild); // Reset form form.reset(); // Auto-close after 3 seconds setTimeout(() => { this.closeWidget(); successMsg.remove(); }, 3000); } catch (error) { // Show error message const errorMsg = document.createElement('div'); errorMsg.className = 'helping-desk-message error'; errorMsg.textContent = `❌ Error: ${error.message || 'Failed to create ticket'}`; form.insertBefore(errorMsg, form.firstChild); } finally { submitBtn.disabled = false; submitBtn.textContent = 'Submit Ticket'; } } /** * Destroy the widget */ destroy() { if (this.container) { this.container.remove(); this.container = null; this.bubble = null; this.widget = null; } } }