UNPKG

@vitalwall/client

Version:

VitalWall client library for vanilla JavaScript applications

812 lines (793 loc) 30.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; // Browser environment detection const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; class VitalWall { constructor() { this.apiKey = ''; this.baseUrl = ''; this.domain = ''; this.projectId = ''; this.debug = false; this.initialized = false; this.initializationPromise = null; this.currentWallId = null; this.accessToken = null; this.refreshToken = null; this.tokenExpiryTime = null; this.subscription = null; this.initializedWalls = new Set(); this.mutationObserver = null; this.readyCallbacks = []; } init(config) { return __awaiter(this, void 0, void 0, function* () { if (!config.apiKey) { this.error('VitalWall: apiKey is required'); throw new Error('VitalWall: apiKey is required'); } this.apiKey = config.apiKey; this.baseUrl = 'https://clyfgobyllqtvmfchczj.supabase.co/functions/v1'; // Auto-detect domain in browser, require explicit domain in Node.js for security if (isBrowser) { this.domain = window.location.hostname; } else { if (!config.domain) { this.error('VitalWall: domain is required when running in Node.js environment'); throw new Error('VitalWall: domain is required when running in Node.js environment'); } this.domain = config.domain; } this.projectId = this.baseUrl.split('/')[2].split('.')[0]; this.debug = config.debug || false; this.currentWallId = config.wallId || null; this.log('VitalWall initialized with domain:', this.domain); // Try to load existing token (browser only) if (isBrowser) { const localStorage = window.localStorage; const storedToken = localStorage.getItem('vitalwall-token'); if (storedToken) { try { const tokenData = JSON.parse(storedToken); if (tokenData.domain === this.domain && Date.now() < tokenData.expiryTime) { this.log('Using stored token'); this.accessToken = tokenData.accessToken; this.refreshToken = tokenData.refreshToken; this.tokenExpiryTime = tokenData.expiryTime; if (!this.currentWallId && tokenData.wallId) { this.currentWallId = tokenData.wallId; } this.scheduleTokenRefresh(); this.initialized = true; this.setupWallObserver(); this.setupTracking(); this.dispatchReadyEvent(); this.checkForWalls(); return; } } catch (e) { this.error('Error parsing stored token:', e); } } } this.accessToken = null; this.refreshToken = null; this.tokenExpiryTime = null; // Initialize tokens and setup this.initializationPromise = this.initializeTokens().then(() => { if (isBrowser) { this.setupWallObserver(); this.setupTracking(); this.checkForWalls(); } this.initialized = true; this.dispatchReadyEvent(); }); return this.initializationPromise; }); } addItem(item) { return __awaiter(this, void 0, void 0, function* () { const wallId = item.wallId || this.currentWallId; if (!wallId) { this.error('No wall ID specified and no current wall set'); return undefined; } return new Promise((resolve, reject) => { this.sendUpdate(item.content, item.isHtml || false, wallId, (itemId) => { resolve(itemId); }, (error) => { reject(error); }); }); }); } addTextItem(content, wallId) { return __awaiter(this, void 0, void 0, function* () { return this.addItem({ content, isHtml: false, wallId }); }); } addHtmlItem(content, wallId) { return __awaiter(this, void 0, void 0, function* () { return this.addItem({ content, isHtml: true, wallId }); }); } setCurrentWall(wallId) { this.currentWallId = wallId; // Update stored token with new wall ID (browser only) if (isBrowser) { const storedToken = localStorage.getItem('vitalwall-token'); if (storedToken) { try { const tokenData = JSON.parse(storedToken); tokenData.wallId = wallId; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } catch (e) { this.error('Error updating stored token with wall ID:', e); } } } } getCurrentWall() { return this.currentWallId; } isInitialized() { return this.initialized; } onReady(callback) { if (this.initialized) { callback(); } else { this.readyCallbacks.push(callback); } } cleanup() { this.cleanupAllWalls(); if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } if (this.subscription) { this.subscription.unsubscribe(); this.subscription = null; } this.initialized = false; this.initializationPromise = null; } log(...args) { if (this.debug) { console.log('[VitalWall]', ...args); } } error(...args) { if (this.debug) { console.error('[VitalWall]', ...args); } } dispatchReadyEvent() { if (isBrowser) { window.dispatchEvent(new Event('vitalwall-loaded')); } this.readyCallbacks.forEach(callback => callback()); this.readyCallbacks = []; } checkForWalls() { if (!isBrowser) return; const maxAttempts = 20; // 10 seconds total let attempts = 0; const check = () => { if (attempts >= maxAttempts) return; attempts++; const walls = document.querySelectorAll('[data-vital-wall]:not([data-vital-wall-initialized])'); if (walls.length > 0) { this.initializeWalls(); return; } // Schedule next check setTimeout(check, 500); }; // Start checking check(); } initializeWalls() { if (!isBrowser) return; const wallElements = document.querySelectorAll('[data-vital-wall]:not([data-vital-wall-initialized])'); wallElements.forEach(element => this.initializeWall(element)); } setupWallObserver() { if (!isBrowser || !document.body) return; this.mutationObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.hasAttribute('data-vital-wall')) { this.initializeWall(element); } // Check for child elements with data-vital-wall const children = element.querySelectorAll('[data-vital-wall]'); children.forEach(child => this.initializeWall(child)); } }); }); }); this.mutationObserver.observe(document.body, { childList: true, subtree: true }); } initializeWall(element) { if (!isBrowser || !element || element.hasAttribute('data-vital-wall-initialized')) { return; } // Get wall ID from data attribute const wallId = element.getAttribute('data-vital-wall'); if (!wallId) { this.error('data-vital-wall attribute must contain a wall ID'); return; } if (!this.currentWallId) { this.currentWallId = wallId; } // Store the wall ID with the token data (browser only) if (isBrowser) { const localStorage = window.localStorage; const storedToken = localStorage.getItem('vitalwall-token'); if (storedToken) { try { const tokenData = JSON.parse(storedToken); if (!tokenData.wallId) { tokenData.wallId = wallId; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } } catch (e) { this.error('Error updating stored token:', e); } } } // Style the element and create container this.styleWallElement(element); // Create container const container = document.createElement('div'); container.className = 'vital-wall-container'; container.style.cssText = ` width: 100%; max-width: 100%; box-sizing: border-box; position: relative; overflow: auto; `; // Add loading state container.innerHTML = '<div class="wall-item">Loading...</div>'; // Clear element and add container element.innerHTML = ''; element.appendChild(container); // Add base styles this.addBaseStyles(); // Store cleanup function element._cleanup = () => { if (this.subscription) { this.subscription.unsubscribe(); this.subscription = null; } }; // Add mutation observer to detect when element is removed if (isBrowser && element.parentNode) { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.removedNodes.forEach((node) => { if (node === element && element._cleanup) { element._cleanup(); } }); }); }); observer.observe(element.parentNode, { childList: true }); } // Load items and setup WebSocket this.loadItems(wallId, container); this.setupWebSocket(wallId, container); // Mark as initialized element.setAttribute('data-vital-wall-initialized', 'true'); this.initializedWalls.add(element); } styleWallElement(element) { element.style.cssText = ` display: block; width: 100%; max-width: 100%; overflow: auto; box-sizing: border-box; position: relative; `; } addBaseStyles() { if (!isBrowser) return; // Check if styles already added if (document.getElementById('vitalwall-styles')) return; const style = document.createElement('style'); style.id = 'vitalwall-styles'; style.textContent = ` .vital-wall-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; position: relative; } .wall-item { background: white; border-radius: 4px; padding: 10px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-sizing: border-box; } /* List layout specific styles */ .vital-wall-container.list-layout { display: flex; flex-direction: column; gap: 1rem; overflow-y: auto; height: 100%; } /* Grid layout specific styles */ .vital-wall-container.grid-layout { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; padding: 1rem; } .vital-wall-container.grid-layout .wall-item { height: 100%; } /* Ticker-specific styles */ .ticker-viewport { position: absolute; top: 0; left: 0; right: 0; overflow: hidden; } .ticker-scroll { position: absolute; top: 0; left: 0; right: 0; display: flex; flex-direction: row-reverse; gap: 1rem; padding: 1rem; overflow-x: auto; } .ticker-scroll .wall-item { flex: 0 0 auto; min-width: 200px; max-width: 300px; } .vital-status-container { position: absolute; bottom: 8px; left: 8px; right: 8px; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #666; z-index: 1000; } `; document.head.appendChild(style); } loadItems(wallId, container) { return __awaiter(this, void 0, void 0, function* () { if (!this.accessToken) { this.error('No access token available'); return; } try { const response = yield fetch(`${this.baseUrl}/wall-items?wall_id=${wallId}`, { headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Origin': isBrowser ? window.location.origin : this.domain } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = yield response.json(); if (data.error) throw new Error(data.error); this.renderItems(data, container); } catch (error) { this.error('Error loading items:', error); container.innerHTML = '<div class="wall-item">Error loading wall data</div>'; } }); } setupWebSocket(wallId, container) { if (!isBrowser) return; const anonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNseWZnb2J5bGxxdHZtZmNoY3pqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjUxNDU3MDYsImV4cCI6MjA0MDcyMTcwNn0.yODfQfQaKeHy3c17uKfJx_wED8cwJ4TK-R4b0qoZ7pM'; const initializeSupabase = () => { var _a; if ((_a = window.supabase) === null || _a === void 0 ? void 0 : _a.createClient) { window.supabase = window.supabase.createClient(`https://${this.projectId}.supabase.co`, anonKey); this.setupRealtimeSubscription(wallId, container); } }; // Load Supabase client if not already loaded if (!window.supabase) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2'; script.onload = () => { initializeSupabase(); }; document.head.appendChild(script); } else { initializeSupabase(); } } setupRealtimeSubscription(wallId, container) { if (!isBrowser) return; this.log('Setting up realtime subscription for wall:', wallId); const channelName = `wall:${wallId}`; const subscription = window.supabase .channel(channelName) .on('postgres_changes', { event: '*', schema: 'public', table: 'wall_items', filter: `wall_id=eq.${wallId}` }, (payload) => { this.log('Received realtime update:', payload); this.loadItems(wallId, container); }) .subscribe((status) => { this.log('Subscription status:', status); }); this.subscription = subscription; } renderItems(data, container) { const { items, config } = data; this.log('Rendering items:', (items === null || items === void 0 ? void 0 : items.length) || 0); if (!items || items.length === 0) { container.innerHTML = '<div class="wall-item">No items to display</div>'; return; } container.className = `vital-wall-container ${config.layout || 'list'}-layout`; if (config.layout === 'ticker') { this.renderTickerLayout(items, container); } else { this.renderStandardLayout(items, container); } this.addStatusContainer(container); } renderTickerLayout(items, container) { if (!isBrowser) return; const viewport = document.createElement('div'); viewport.className = 'ticker-viewport'; const tickerScroll = document.createElement('div'); tickerScroll.className = 'ticker-scroll'; items.forEach(item => { const itemElement = document.createElement('div'); itemElement.className = 'wall-item'; if (item.is_html) { itemElement.innerHTML = item.content; } else { itemElement.innerHTML = `<p>${item.content}</p>`; } tickerScroll.appendChild(itemElement); }); viewport.appendChild(tickerScroll); container.innerHTML = ''; container.appendChild(viewport); } renderStandardLayout(items, container) { if (!isBrowser) return; container.innerHTML = ''; items.forEach(item => { const itemElement = document.createElement('div'); itemElement.className = 'wall-item'; if (item.is_html) { itemElement.innerHTML = item.content; } else { itemElement.innerHTML = `<p>${item.content}</p>`; } container.appendChild(itemElement); }); } addStatusContainer(container) { if (!isBrowser) return; const statusContainer = document.createElement('div'); statusContainer.className = 'vital-status-container'; const connectionStatus = document.createElement('div'); connectionStatus.className = 'vital-connection'; connectionStatus.innerHTML = ` <span class="connection-dot" style=" display: inline-block; width: 8px; height: 8px; background-color: #4CAF50; border-radius: 50%; margin-right: 4px; "></span> <span>Connected</span> `; const poweredBy = document.createElement('div'); poweredBy.className = 'vital-powered'; poweredBy.innerHTML = 'Powered by <span style="color: rgb(237, 28, 36)">Vital</span><span style="color: rgb(51, 51, 51); font-family: Cal Sans, sans-serif">Wall</span>'; poweredBy.style.cssText = ` font-weight: 500; cursor: pointer; `; poweredBy.onclick = () => window.open('https://vital.io', '_blank'); statusContainer.appendChild(connectionStatus); statusContainer.appendChild(poweredBy); container.appendChild(statusContainer); } initializeTokens() { return __awaiter(this, void 0, void 0, function* () { this.log('Initializing tokens...'); try { const response = yield fetch(`${this.baseUrl}/generate-wall-token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: this.apiKey, domain: this.domain }) }); this.log('Token response status:', response.status); const data = yield response.json(); this.log('Token response data:', data); if (data.error) throw new Error(data.error); this.accessToken = data.access_token; this.refreshToken = data.refresh_token; this.tokenExpiryTime = Date.now() + (data.expires_in * 1000); this.log('Tokens initialized successfully'); // Store token data (browser only) if (isBrowser) { const tokenData = { accessToken: this.accessToken, refreshToken: this.refreshToken, expiryTime: this.tokenExpiryTime, domain: this.domain, wallId: this.currentWallId || undefined }; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } this.scheduleTokenRefresh(); } catch (error) { this.error('Error initializing tokens:', error); throw error; } }); } scheduleTokenRefresh() { if (!this.tokenExpiryTime) return; const REFRESH_BUFFER = 5 * 60 * 1000; // 5 minutes const timeUntilRefresh = this.tokenExpiryTime - Date.now() - REFRESH_BUFFER; setTimeout(() => __awaiter(this, void 0, void 0, function* () { try { yield this.refreshAccessToken(); } catch (error) { this.error('Error refreshing token:', error); } }), Math.max(0, timeUntilRefresh)); } refreshAccessToken() { return __awaiter(this, void 0, void 0, function* () { if (!this.refreshToken) { throw new Error('No refresh token available'); } const response = yield fetch(`${this.baseUrl}/refresh-wall-token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: this.refreshToken, domain: this.domain }) }); const data = yield response.json(); if (data.error) throw new Error(data.error); this.accessToken = data.access_token; this.tokenExpiryTime = Date.now() + (data.expires_in * 1000); // Update stored token data (browser only) if (isBrowser) { const tokenData = { accessToken: this.accessToken, refreshToken: this.refreshToken, expiryTime: this.tokenExpiryTime, domain: this.domain, wallId: this.currentWallId || undefined }; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } this.scheduleTokenRefresh(); }); } setupTracking() { if (!isBrowser) return; // Track existing elements const vitalItems = document.querySelectorAll('[data-vital-item]'); vitalItems.forEach(element => this.trackVitalItem(element)); // Set up observer for dynamically added elements this.setupTrackingMutationObserver(); } setupTrackingMutationObserver() { if (!isBrowser || !document.body) return; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.hasAttribute('data-vital-item')) { this.trackVitalItem(element); } const children = element.querySelectorAll('[data-vital-item]'); children.forEach(child => this.trackVitalItem(child)); } }); }); }); try { observer.observe(document.body, { childList: true, subtree: true }); } catch (error) { this.error('Error setting up tracking observer:', error); } } trackVitalItem(element) { if (!isBrowser) return; let content = element.getAttribute('data-vital-item') || ''; const isHtml = element.getAttribute('data-vital-html') === 'true'; const isAuto = element.getAttribute('data-vital-auto') === 'true'; const sendContentId = element.getAttribute('data-vital-send-content'); // If sendContentId is provided, find and use that element's content if (sendContentId) { const targetElement = document.getElementById(sendContentId); if (targetElement) { content = isHtml ? targetElement.innerHTML : targetElement.textContent || ''; } } // If no content specified, use the element's HTML or text if (!content) { content = isHtml ? element.innerHTML : element.textContent || ''; } // Don't track empty content if (!content) return; // Send update immediately if auto-tracking is enabled if (isAuto || (!element.getAttribute('data-vital-item') && !sendContentId)) { this.sendUpdate(content, isHtml); return; } // Otherwise, track click events element.addEventListener('click', () => { this.sendUpdate(content, isHtml); }); } sendUpdate(content, isHtml, wallId, onSuccess, onError) { const targetWallId = wallId || this.currentWallId; if (!targetWallId) { const error = new Error('No wall ID specified'); if (onError) onError(error); return; } // For real authentication, we need Bearer token if (!this.accessToken) { const error = new Error('No access token available. VitalWall may not be properly initialized.'); this.error('sendUpdate failed:', error.message); if (onError) onError(error); return; } const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.accessToken}` }; const requestBody = { content: content, isHtml: isHtml, wall_id: targetWallId, domain: this.domain }; this.log('Sending request to:', `${this.baseUrl}/wall-update`); this.log('Request headers:', headers); this.log('Request body:', requestBody); fetch(`${this.baseUrl}/wall-update`, { method: 'POST', headers: headers, body: JSON.stringify(requestBody) }) .then(response => response.json()) .then(data => { if (data.error) throw new Error(data.error); if (onSuccess) onSuccess(data.id); }) .catch(error => { this.error('Error sending update:', error); if (onError) onError(error); }); } cleanupAllWalls() { if (this.initializedWalls) { this.initializedWalls.forEach(element => { if (element._cleanup) { element._cleanup(); } element.removeAttribute('data-vital-wall-initialized'); }); this.initializedWalls.clear(); } } } // For convenient instantiation const createVitalWall = () => new VitalWall(); exports.createVitalWall = createVitalWall; exports.default = VitalWall;