@helping-desk/web-sdk
Version:
Web SDK for Helping Desk - Ticket creation and support widget integration
462 lines (431 loc) • 13.4 kB
JavaScript
/**
* 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;
}
}
}