PageSourceSearch

https://datamossa.co.th/script.js

js datamossa.co.th collected 2026-10-08 18:31:03 UTC 26,620 bytes, 731 lines download raw bytes

1// Language Toggle Functionality
2const langToggle = document.getElementById('langToggle');
3const htmlElement = document.documentElement;
4
5// Check for saved language preference or default to English
6let currentLang = localStorage.getItem('lang') || 'en';
7htmlElement.setAttribute('data-lang', currentLang);
8htmlElement.setAttribute('lang', currentLang);
9
10// Translation function
11function translatePage(lang) {
12    const elements = document.querySelectorAll('[data-i18n]');
13    elements.forEach(element => {
14        const key = element.getAttribute('data-i18n');
15        const keys = key.split('.');
16        let translation = translations[lang];
17        
18        for (let k of keys) {
19            if (translation && translation[k]) {
20                translation = translation[k];
21            } else {
22                translation = null;
23                break;
24            }
25        }
26        
27        if (translation) {
28            if (element.tagName === 'INPUT' && element.hasAttribute('placeholder')) {
29                element.placeholder = translation;
30            } else if (element.tagName === 'OPTION') {
31                element.textContent = translation;
32            } else {
33                element.textContent = translation;
34            }
35        }
36    });
37    
38    // Handle placeholder attributes
39    const placeholderElements = document.querySelectorAll('[data-i18n-placeholder]');
40    placeholderElements.forEach(element => {
41        const key = element.getAttribute('data-i18n-placeholder');
42        const keys = key.split('.');
43        let translation = translations[lang];
44        
45        for (let k of keys) {
46            if (translation && translation[k]) {
47                translation = translation[k];
48            } else {
49                translation = null;
50                break;
51            }
52        }
53        
54        if (translation) {
55            element.placeholder = translation;
56        }
57    });
58    
59    // Update language toggle button
60    if (langToggle) {
61        langToggle.querySelector('.lang-text').textContent = lang === 'en' ? 'TH' : 'EN';
62    }
63    
64    // Update HTML lang attribute
65    htmlElement.setAttribute('lang', lang);
66    htmlElement.setAttribute('data-lang', lang);
67    
68    // Update form placeholders manually (for fields without data-i18n-placeholder)
69    updateFormPlaceholders(lang);
70}
71
72// Update form placeholders when language changes
73function updateFormPlaceholders(lang) {
74    // Contact form placeholders
75    const nameField = document.getElementById('name');
76    if (nameField) nameField.placeholder = translations[lang].contact.form.name;
77    const emailField = document.getElementById('email');
78    if (emailField) emailField.placeholder = translations[lang].contact.form.email;
79    const phoneField = document.getElementById('phone');
80    if (phoneField) phoneField.placeholder = translations[lang].contact.form.phone;
81    const messageField = document.getElementById('message');
82    if (messageField) messageField.placeholder = translations[lang].contact.form.message;
83    const serviceField = document.getElementById('service');
84    if (serviceField) {
85        const defaultOption = serviceField.querySelector('option[value=""]');
86        if (defaultOption) defaultOption.textContent = translations[lang].contact.form.selectService;
87    }
88    
89    // Chat form placeholders
90    const chatName = document.getElementById('chatName');
91    if (chatName) chatName.placeholder = translations[lang].chat.formName;
92    const chatEmail = document.getElementById('chatEmail');
93    if (chatEmail) chatEmail.placeholder = translations[lang].chat.formEmail;
94    const chatPhone = document.getElementById('chatPhone');
95    if (chatPhone) chatPhone.placeholder = translations[lang].chat.formPhone;
96    
97    // Chat input placeholder
98    const slackChatInput = document.getElementById('slackChatInput');
99    if (slackChatInput) {
100        slackChatInput.placeholder = translations[lang].chat.placeholder;
101    }
102}
103
104// Initialize language - wait for DOM to be ready
105if (document.readyState === 'loading') {
106    document.addEventListener('DOMContentLoaded', () => {
107        translatePage(currentLang);
108    });
109} else {
110    translatePage(currentLang);
111}
112
113if (langToggle) {
114    langToggle.addEventListener('click', () => {
115        currentLang = currentLang === 'en' ? 'th' : 'en';
116        translatePage(currentLang);
117        localStorage.setItem('lang', currentLang);
118    });
119}
120
121// Theme Toggle Functionality
122const themeToggle = document.getElementById('themeToggle');
123
124// Check for saved theme preference or default to dark mode
125const currentTheme = localStorage.getItem('theme') || 'dark';
126htmlElement.setAttribute('data-theme', currentTheme);
127
128if (themeToggle) {
129    themeToggle.addEventListener('click', () => {
130        const currentTheme = htmlElement.getAttribute('data-theme');
131        const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
132        
133        htmlElement.setAttribute('data-theme', newTheme);
134        localStorage.setItem('theme', newTheme);
135    });
136}
137
138// Mobile Menu Toggle
139const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
140const navMenu = document.querySelector('.nav-menu');
141
142if (mobileMenuToggle) {
143    mobileMenuToggle.addEventListener('click', () => {
144        navMenu.classList.toggle('active');
145    });
146}
147
148// Close mobile menu when clicking on a link
149const navLinks = document.querySelectorAll('.nav-menu a');
150navLinks.forEach(link => {
151    link.addEventListener('click', () => {
152        navMenu.classList.remove('active');
153    });
154});
155
156// Slack Chat Widget
157const slackChatToggle = document.getElementById('slackChatToggle');
158const slackChatWindow = document.getElementById('slackChatWindow');
159const slackChatClose = document.getElementById('slackChatClose');
160const slackChatInput = document.getElementById('slackChatInput');
161const slackChatSend = document.getElementById('slackChatSend');
162// slackChatBody is now defined above with getElementById
163
164// Ensure chat window is closed by default on page load
165// Run this immediately and also on DOMContentLoaded to catch any timing issues
166function ensureChatClosed() {
167    if (slackChatWindow) {
168        slackChatWindow.classList.remove('active');
169        slackChatWindow.style.display = 'none';
170        slackChatWindow.style.visibility = 'hidden';
171    }
172}
173
174// Run immediately
175ensureChatClosed();
176
177// Also run when DOM is ready
178if (document.readyState === 'loading') {
179    document.addEventListener('DOMContentLoaded', ensureChatClosed);
180} else {
181    ensureChatClosed();
182}
183
184// Toggle chat window
185if (slackChatToggle) {
186    slackChatToggle.addEventListener('click', () => {
187        const isOpening = !slackChatWindow.classList.contains('active');
188        slackChatWindow.classList.toggle('active');
189        
190        // Explicitly set display style to ensure it works
191        if (isOpening) {
192            slackChatWindow.style.display = 'flex';
193            slackChatWindow.style.visibility = 'visible';
194            document.body.classList.add('chat-open');
195        } else {
196            slackChatWindow.style.display = 'none';
197            slackChatWindow.style.visibility = 'hidden';
198            document.body.classList.remove('chat-open');
199        }
200    });
201}
202
203if (slackChatClose) {
204    slackChatClose.addEventListener('click', () => {
205        slackChatWindow.classList.remove('active');
206        slackChatWindow.style.display = 'none'; // Explicitly hide
207        slackChatWindow.style.visibility = 'hidden'; // Also hide visibility
208        // Remove body scroll lock
209        document.body.classList.remove('chat-open');
210        // Reset form when closing (if chat hasn't started)
211        if (!chatThreadTs) {
212            resetChatForm();
213        }
214    });
215}
216
217// Handle chat form submission (collect user info before starting chat)
218if (chatFormSubmit && chatName && chatEmail) {
219    chatFormSubmit.addEventListener('click', (e) => {
220        e.preventDefault();
221        
222        const name = chatName.value.trim();
223        const email = chatEmail.value.trim();
224        const phone = chatPhone.value.trim();
225        
226        // Validate required fields
227        if (!name || !email) {
228            alert(translations[currentLang].chat.formError || 'Please provide your name and email');
229            return;
230        }
231        
232        // Validate email format
233        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
234        if (!emailRegex.test(email)) {
235            alert(translations[currentLang].chat.emailError || 'Please provide a valid email address');
236            return;
237        }
238        
239        // Store user info
240        chatUserInfo = { name, email, phone: phone || undefined };
241        
242        // Hide form, show chat body and input
243        slackChatForm.style.display = 'none';
244        slackChatBody.style.display = 'block';
245        slackChatInputWrapper.style.display = 'flex';
246        
247        // Focus on input
248        slackChatInput.focus();
249    });
250}
251
252// Reset chat form (when closing before starting chat)
253function resetChatForm() {
254    if (slackChatForm && slackChatBody && slackChatInputWrapper) {
255        slackChatForm.style.display = 'block';
256        slackChatBody.style.display = 'none';
257        slackChatInputWrapper.style.display = 'none';
258        if (chatName) chatName.value = '';
259        if (chatEmail) chatEmail.value = '';
260        if (chatPhone) chatPhone.value = '';
261        chatUserInfo = null;
262        isFirstMessage = true;
263    }
264}
265
266// API Configuration - Now using server-side API with Slack App integration
267const API_BASE = '/api';
268
269// Set version and copyright year on page load
270// Check if DOMContentLoaded already fired, if so run immediately
271if (document.readyState === 'loading') {
272    document.addEventListener('DOMContentLoaded', setVersionAndYear);
273} else {
274    setVersionAndYear();
275}
276
277function setVersionAndYear() {
278    // Set current year
279    const currentYear = new Date().getFullYear();
280    const currentYearEl = document.getElementById('currentYear');
281    if (currentYearEl) {
282        currentYearEl.textContent = currentYear;
283    }
284    
285    // Set copyright start year (2024)
286    const copyrightYearEl = document.getElementById('copyrightYear');
287    if (copyrightYearEl) {
288        copyrightYearEl.textContent = '2024';
289    }
290    
291    // Fetch version from server
292    fetch('/api/version')
293        .then(response => response.json())
294        .then(data => {
295            const versionEl = document.getElementById('appVersion');
296            if (versionEl && data.version) {
297                versionEl.textContent = data.version;
298            }
299        })
300        .catch(error => {
301            console.warn('Failed to fetch version:', error);
302            // Fallback to package.json version if available
303            const versionEl = document.getElementById('appVersion');
304            if (versionEl) {
305                versionEl.textContent = '1.0.0';
306            }
307        });
308}
309
310// Store thread timestamp and channel for chat conversations
311let chatThreadTs = null;
312let chatChannel = null;
313let lastReplyCheck = null;
314let replyCheckInterval = null;
315let lastUserMessageTime = null; // Track when user sends messages
316let chatUserInfo = null; // Store user info (name, email, phone) for the chat session
317let isFirstMessage = true; // Track if this is the first message in the conversation
318
319// Function to send message via API (uses Slack App)
320async function sendToSlack(message, userInfo = {}, endpoint = 'contact', threadTs = null) {
321    try {
322        const response = await fetch(`${API_BASE}/${endpoint}`, {
323            method: 'POST',
324            headers: {
325                'Content-Type': 'application/json',
326            },
327            body: JSON.stringify({
328                message,
329                userInfo: userInfo, // Send as nested object for chat endpoint
330                threadTs: threadTs
331            })
332        });
333
334        const data = await response.json();
335        
336        if (!response.ok) {
337            throw new Error(data.message || 'Failed to send message');
338        }
339
340        // For chat, return success, threadTs, and channel
341        if (endpoint === 'chat' && data.threadTs) {
342            return { 
343                success: data.success, 
344                threadTs: data.threadTs,
345                channel: data.channel
346            };
347        }
348
349        return { success: data.success };
350    } catch (error) {
351        console.error('Error sending message:', error);
352        return { success: false, error: error.message };
353    }
354}
355
356// Helper function to get Gravatar URL from email
357function getGravatarUrl(email) {
358    if (!email) return null;
359    // Simple MD5 hash (for client-side, we'll use a library or generate on server)
360    // For now, we'll rely on server-provided avatar URLs
361    return null;
362}
363
364// Add message to chat window
365function addChatMessage(message, isUser = false, senderName = null, senderAvatar = null) {
366    const messageDiv = document.createElement('div');
367    messageDiv.className = `slack-chat-message-wrapper ${isUser ? 'slack-chat-user-wrapper' : 'slack-chat-bot-wrapper'}`;
368    
369    let messageContent = '';
370    
371    // If there's a sender name/avatar (from Slack), show it with avatar
372    if (senderName && !isUser) {
373        const avatarHtml = senderAvatar 
374            ? `<img src="${senderAvatar}" alt="${senderName}" class="slack-chat-avatar" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';" />`
375            : '';
376        const fallbackInitial = senderName.charAt(0).toUpperCase();
377        messageContent = `
378            <div class="slack-chat-avatar-container">
379                ${avatarHtml}
380                <div class="slack-chat-avatar-fallback" style="${senderAvatar ? 'display:none;' : ''}">${fallbackInitial}</div>
381            </div>
382            <div class="slack-chat-message ${isUser ? 'slack-chat-user' : 'slack-chat-bot'}">
383                <div class="slack-chat-sender">${senderName}</div>
384                <p>${message}</p>
385            </div>
386        `;
387    } else {
388        // User's own messages - show avatar if we have user info
389        if (isUser && chatUserInfo) {
390            const userInitial = (chatUserInfo.name || 'U').charAt(0).toUpperCase();
391            // For user's own messages, we'll use a simple colored initial circle
392            // Gravatar would require MD5 hashing which we'll handle server-side if needed
393            messageContent = `
394                <div class="slack-chat-message ${isUser ? 'slack-chat-user' : 'slack-chat-bot'}">
395                    <p>${message}</p>
396                </div>
397                <div class="slack-chat-avatar-container">
398                    <div class="slack-chat-avatar-fallback slack-chat-user-avatar">${userInitial}</div>
399                </div>
400            `;
401        } else {
402            messageContent = `
403                <div class="slack-chat-message ${isUser ? 'slack-chat-user' : 'slack-chat-bot'}">
404                    <p>${message}</p>
405                </div>
406            `;
407        }
408    }
409    
410    messageDiv.innerHTML = messageContent;
411    slackChatBody.appendChild(messageDiv);
412    slackChatBody.scrollTop = slackChatBody.scrollHeight;
413}
414
415// Handle chat send
416if (slackChatSend && slackChatInput) {
417    slackChatSend.addEventListener('click', async () => {
418        const message = slackChatInput.value.trim();
419        if (!message) return;
420
421        // Disable input while sending
422        slackChatInput.disabled = true;
423        slackChatSend.disabled = true;
424
425        // Add user message to chat
426        addChatMessage(message, true);
427        lastUserMessageTime = new Date().toISOString();
428        slackChatInput.value = '';
429
430        // Show typing indicator
431        const typingIndicator = document.createElement('div');
432        typingIndicator.className = 'slack-chat-message slack-chat-bot';
433        typingIndicator.innerHTML = '<p><em>Typing...</em></p>';
434        slackChatBody.appendChild(typingIndicator);
435        slackChatBody.scrollTop = slackChatBody.scrollHeight;
436
437        // Send to Slack via API (include threadTs for conversation continuity and userInfo)
438        const result = await sendToSlack(message, chatUserInfo || {}, 'chat', chatThreadTs);
439
440        // Remove typing indicator
441        typingIndicator.remove();
442
443        // Re-enable input
444        slackChatInput.disabled = false;
445        slackChatSend.disabled = false;
446
447        // Add bot response
448        if (result.success) {
449            // Store thread timestamp and channel for continuing conversation
450            // Always update if we get new values (first message) or keep existing (replies)
451            if (result.threadTs) {
452                // Only update if this is the first message (chatThreadTs is null)
453                // or if we're getting a new thread (shouldn't happen, but handle it)
454                if (!chatThreadTs || result.threadTs !== chatThreadTs) {
455                    chatThreadTs = result.threadTs;
456                }
457            }
458            if (result.channel) {
459                chatChannel = result.channel;
460            }
461            
462            // Start polling for replies if we have a thread and channel
463            if (chatThreadTs && chatChannel) {
464                startReplyPolling();
465            }
466            
467            // Only show thank you message on first message
468            if (isFirstMessage) {
469                addChatMessage(translations[currentLang].chat.received);
470                isFirstMessage = false;
471            }
472        } else {
473            addChatMessage(translations[currentLang].chat.error);
474            console.error('Chat error:', result.error);
475        }
476    });
477
478    // Allow Enter key to send
479    slackChatInput.addEventListener('keypress', (e) => {
480        if (e.key === 'Enter') {
481            slackChatSend.click();
482        }
483    });
484}
485
486// Poll for new replies in Slack thread
487async function checkForReplies() {
488    if (!chatThreadTs || !chatChannel) {
489        console.log('Cannot check for replies: missing threadTs or channel', { chatThreadTs, chatChannel });
490        return;
491    }
492
493    try {
494        const response = await fetch(`${API_BASE}/chat/replies?threadTs=${encodeURIComponent(chatThreadTs)}&channel=${encodeURIComponent(chatChannel)}`);
495        const data = await response.json();
496
497        if (data.success && data.replies && data.replies.length > 0) {
498            // Check if we have new replies (compare timestamps)
499            const newReplies = data.replies.filter(reply => {
500                if (!lastReplyCheck) {
501                    // First check - only show replies newer than the last user message
502                    // We'll track the last reply timestamp
503                    return true;
504                }
505                return new Date(reply.timestamp) > new Date(lastReplyCheck);
506            });
507
508            if (newReplies.length > 0) {
509                // Display new replies in chat with sender names and avatars
510                newReplies.forEach(reply => {
511                    if (reply.text && reply.text.trim()) {
512                        addChatMessage(
513                            reply.text, 
514                            false, 
515                            reply.userName || 'Team Member',
516                            reply.userAvatar || null
517                        );
518                    }
519                });
520                
521                // Update last check time to the most recent reply
522                const mostRecentReply = newReplies[newReplies.length - 1];
523                lastReplyCheck = mostRecentReply.timestamp;
524            }
525        } else if (!data.success) {
526            console.error('Failed to get replies:', data.message);
527        }
528    } catch (error) {
529        console.error('Error checking for replies:', error);
530    }
531}
532
533// Start polling for replies
534function startReplyPolling() {
535    // Clear any existing interval
536    if (replyCheckInterval) {
537        clearInterval(replyCheckInterval);
538    }
539
540    // Check immediately
541    checkForReplies();
542
543    // Then check every 5 seconds
544    replyCheckInterval = setInterval(checkForReplies, 5000);
545}
546
547// Stop polling when chat window is closed
548if (slackChatClose) {
549    slackChatClose.addEventListener('click', () => {
550        if (replyCheckInterval) {
551            clearInterval(replyCheckInterval);
552            replyCheckInterval = null;
553        }
554        lastReplyCheck = null;
555    });
556}
557
558// Client-side form validation
559function validateForm(data) {
560    const errors = [];
561
562    // Name validation
563    if (!data.name || data.name.trim().length < 2 || data.name.trim().length > 100) {
564        errors.push({ field: 'name', message: 'Name must be between 2 and 100 characters' });
565    }
566
567    // Email validation
568    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
569    if (!data.email || !emailRegex.test(data.email)) {
570        errors.push({ field: 'email', message: 'Please provide a valid email address' });
571    }
572
573    // Phone validation (optional but if provided, must be valid)
574    if (data.phone && data.phone.trim()) {
575        const phoneRegex = /^[\d\s\-\+\(\)]+$/;
576        if (!phoneRegex.test(data.phone)) {
577            errors.push({ field: 'phone', message: 'Phone number contains invalid characters' });
578        }
579    }
580
581    // Message validation
582    if (!data.message || data.message.trim().length < 10 || data.message.trim().length > 2000) {
583        errors.push({ field: 'message', message: 'Message must be between 10 and 2000 characters' });
584    }
585
586    return errors;
587}
588
589// Contact Form Handler
590const contactForm = document.getElementById('contactForm');
591const formMessage = document.getElementById('formMessage');
592
593if (contactForm) {
594    contactForm.addEventListener('submit', async (e) => {
595        e.preventDefault();
596
597        const formData = new FormData(contactForm);
598        const data = {
599            name: formData.get('name'),
600            email: formData.get('email'),
601            phone: formData.get('phone'),
602            service: formData.get('service'),
603            message: formData.get('message')
604        };
605
606        // Client-side validation
607        const validationErrors = validateForm(data);
608        if (validationErrors.length > 0) {
609            formMessage.className = 'form-message error';
610            formMessage.textContent = validationErrors[0].message;
611            formMessage.style.display = 'block';
612            
613            // Highlight error field
614            const errorField = contactForm.querySelector(`[name="${validationErrors[0].field}"]`);
615            if (errorField) {
616                errorField.style.borderColor = 'var(--error-color)';
617                errorField.focus();
618                errorField.addEventListener('input', function() {
619                    this.style.borderColor = '';
620                }, { once: true });
621            }
622            return;
623        }
624
625        // Show loading state
626        const submitButton = contactForm.querySelector('.btn-submit');
627        const originalText = translations[currentLang].contact.form.send;
628        const sendingText = translations[currentLang].contact.form.sending;
629        submitButton.textContent = sendingText;
630        submitButton.disabled = true;
631        formMessage.style.display = 'none';
632
633        // Get reCAPTCHA token if configured
634        let recaptchaToken = null;
635        const recaptchaSiteKey = window.RECAPTCHA_SITE_KEY || null;
636        if (recaptchaSiteKey && window.grecaptcha && window.grecaptcha.ready) {
637            try {
638                recaptchaToken = await new Promise((resolve) => {
639                    window.grecaptcha.ready(() => {
640                        window.grecaptcha.execute(recaptchaSiteKey, { action: 'contact' })
641                            .then(resolve)
642                            .catch(() => resolve(null));
643                    });
644                });
645            } catch (e) {
646                console.warn('reCAPTCHA error:', e);
647            }
648        }
649
650        // Send to API (which handles Slack integration)
651        try {
652            const response = await fetch(`${API_BASE}/contact`, {
653                method: 'POST',
654                headers: {
655                    'Content-Type': 'application/json',
656                },
657                body: JSON.stringify({
658                    ...data,
659                    recaptchaToken: recaptchaToken
660                })
661            });
662
663            const result = await response.json();
664
665            // Reset button
666            submitButton.textContent = originalText;
667            submitButton.disabled = false;
668
669            // Show message
670            if (result.success) {
671                formMessage.className = 'form-message success';
672                formMessage.textContent = result.message || translations[currentLang].contact.form.success;
673                contactForm.reset();
674            } else {
675                formMessage.className = 'form-message error';
676                formMessage.textContent = result.message || translations[currentLang].contact.form.error;
677            }
678
679            formMessage.style.display = 'block';
680
681            // Scroll to message
682            formMessage.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
683        } catch (error) {
684            console.error('Form submission error:', error);
685            submitButton.textContent = originalText;
686            submitButton.disabled = false;
687            formMessage.className = 'form-message error';
688            formMessage.textContent = translations[currentLang].contact.form.error;
689            formMessage.style.display = 'block';
690        }
691    });
692}
693
694// Smooth scroll for anchor links
695document.querySelectorAll('a[href^="#"]').forEach(anchor => {
696    anchor.addEventListener('click', function (e) {
697        e.preventDefault();
698        const target = document.querySelector(this.getAttribute('href'));
699        if (target) {
700            const offsetTop = target.offsetTop - 80; // Account for fixed navbar
701            window.scrollTo({
702                top: offsetTop,
703                behavior: 'smooth'
704            });
705        }
706    });
707});
708
709// Add scroll animation on scroll
710const observerOptions = {
711    threshold: 0.1,
712    rootMargin: '0px 0px -50px 0px'
713};
714
715const observer = new IntersectionObserver((entries) => {
716    entries.forEach(entry => {
717        if (entry.isIntersecting) {
718            entry.target.style.opacity = '1';
719            entry.target.style.transform = 'translateY(0)';
720        }
721    });
722}, observerOptions);
723
724// Observe solution cards and feature items
725document.querySelectorAll('.solution-card, .feature-item').forEach(el => {
726    el.style.opacity = '0';
727    el.style.transform = 'translateY(20px)';
728    el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
729    observer.observe(el);
730});
731

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.