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.