/** * TPU Survey - Core Application Logic * API communication, Auth state, Toast notifications, Theme */ const getAppBasePath = () => { const p = window.location.pathname; if (p.includes('/meeting')) return '/meeting'; if (p.includes('/survey')) return '/survey'; return ''; }; const API = { base: `${getAppBasePath()}/api`, async request(endpoint, options = {}) { const url = `${this.base}/${endpoint}`; const config = { headers: { 'Content-Type': 'application/json' }, ...options }; if (options.body && typeof options.body === 'object') { config.body = JSON.stringify(options.body); } try { const res = await fetch(url, config); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'เกิดข้อผิดพลาด'); return data; } catch (err) { if (err.message === 'Failed to fetch') throw new Error('ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้'); throw err; } }, get(endpoint) { return this.request(endpoint); }, post(endpoint, body) { return this.request(endpoint, { method: 'POST', body }); }, upload(endpoint, formData) { const url = `${this.base}/${endpoint}`; return fetch(url, { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.error) throw new Error(data.error); return data; }); } }; // ============================================================ // Universal Clipboard Helper (HTTP & HTTPS compatible) // ============================================================ function copyTextToClipboard(text, btnElement) { if (!text) return; const showSuccess = () => { if (typeof Toast !== 'undefined') Toast.success('คัดลอกลิงก์สำเร็จแล้ว!'); if (btnElement) { const origHtml = btnElement.innerHTML; btnElement.innerHTML = '✅ คัดลอกแล้ว!'; btnElement.style.backgroundColor = '#16a34a'; btnElement.style.color = '#ffffff'; setTimeout(() => { btnElement.innerHTML = origHtml; btnElement.style.backgroundColor = ''; btnElement.style.color = ''; }, 2000); } }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(showSuccess).catch(() => fallbackCopy(text, showSuccess)); } else { fallbackCopy(text, showSuccess); } } function fallbackCopy(text, callback) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.top = '0'; textArea.style.left = '0'; textArea.style.width = '2em'; textArea.style.height = '2em'; textArea.style.padding = '0'; textArea.style.border = 'none'; textArea.style.outline = 'none'; textArea.style.boxShadow = 'none'; textArea.style.background = 'transparent'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { const successful = document.execCommand('copy'); if (successful && callback) { callback(); } else if (!successful && typeof Toast !== 'undefined') { Toast.error('กรุณากดคัดลอกด้วยตนเอง'); } } catch (err) { if (typeof Toast !== 'undefined') Toast.error('ไม่สามารถคัดลอกได้'); } document.body.removeChild(textArea); } // ============================================================ // Auth State // ============================================================ const Auth = { user: null, async check() { try { const data = await API.get('auth.php?action=check'); if (data.loggedIn) { this.user = data.user; return true; } } catch (e) {} this.user = null; return false; }, async login(username, password) { const data = await API.post('auth.php?action=login', { username, password }); this.user = data.user; return data; }, async logout() { await API.post('auth.php?action=logout', {}); this.user = null; window.location.href = `${getAppBasePath()}/index.html`; }, isAdmin() { return this.user?.role === 'admin'; }, isTeacher() { return this.user?.role === 'teacher'; } }; // ============================================================ // Sound Effects (Web Audio API) // ============================================================ const SoundFX = { ctx: null, getCtx() { if (!this.ctx) { this.ctx = new (window.AudioContext || window.webkitAudioContext)(); } return this.ctx; }, // เสียง OK — 2 โน้ตสั้น ไพเราะ playSuccess() { try { const ctx = this.getCtx(); const now = ctx.currentTime; // Note 1: C5 (523 Hz) const osc1 = ctx.createOscillator(); const gain1 = ctx.createGain(); osc1.type = 'sine'; osc1.frequency.setValueAtTime(523, now); gain1.gain.setValueAtTime(0.3, now); gain1.gain.exponentialRampToValueAtTime(0.01, now + 0.15); osc1.connect(gain1); gain1.connect(ctx.destination); osc1.start(now); osc1.stop(now + 0.15); // Note 2: E5 (659 Hz) — ช้ากว่าเล็กน้อย const osc2 = ctx.createOscillator(); const gain2 = ctx.createGain(); osc2.type = 'sine'; osc2.frequency.setValueAtTime(659, now + 0.1); gain2.gain.setValueAtTime(0, now); gain2.gain.setValueAtTime(0.3, now + 0.1); gain2.gain.exponentialRampToValueAtTime(0.01, now + 0.3); osc2.connect(gain2); gain2.connect(ctx.destination); osc2.start(now + 0.1); osc2.stop(now + 0.3); // Note 3: G5 (784 Hz) — จบด้วยโน้ตสูง const osc3 = ctx.createOscillator(); const gain3 = ctx.createGain(); osc3.type = 'sine'; osc3.frequency.setValueAtTime(784, now + 0.2); gain3.gain.setValueAtTime(0, now); gain3.gain.setValueAtTime(0.25, now + 0.2); gain3.gain.exponentialRampToValueAtTime(0.01, now + 0.45); osc3.connect(gain3); gain3.connect(ctx.destination); osc3.start(now + 0.2); osc3.stop(now + 0.45); } catch (e) { // AudioContext not supported } }, // เสียง Error — โทนต่ำ playError() { try { const ctx = this.getCtx(); const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'square'; osc.frequency.setValueAtTime(200, now); osc.frequency.linearRampToValueAtTime(150, now + 0.3); gain.gain.setValueAtTime(0.15, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3); osc.connect(gain); gain.connect(ctx.destination); osc.start(now); osc.stop(now + 0.3); } catch (e) {} } }; // ============================================================ // Toast Notifications // ============================================================ const Toast = { container: null, init() { if (!this.container) { this.container = document.createElement('div'); this.container.className = 'toast-container'; document.body.appendChild(this.container); } }, show(message, type = 'info', duration = 4000) { this.init(); const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; toast.innerHTML = `${icons[type] || 'ℹ️'}${message}`; this.container.appendChild(toast); setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); }, success(msg) { this.show(msg, 'success'); SoundFX.playSuccess(); }, error(msg) { this.show(msg, 'error'); SoundFX.playError(); }, warning(msg) { this.show(msg, 'warning'); }, info(msg) { this.show(msg, 'info'); } }; // ============================================================ // Theme Management (Light Mode Only — ม.ตาปี) // ============================================================ const Theme = { init() { /* Light mode only */ }, toggle() { /* Disabled */ } }; // ============================================================ // Modal Helper // ============================================================ const Modal = { show(id) { const el = document.getElementById(id); if (el) el.classList.add('active'); }, hide(id) { const el = document.getElementById(id); if (el) el.classList.remove('active'); }, create(title, bodyHTML, footerHTML = '', size = '') { const id = 'modal-' + Date.now(); const div = document.createElement('div'); div.id = id; div.className = 'modal-overlay'; div.innerHTML = ` `; document.body.appendChild(div); div.classList.add('active'); div.addEventListener('click', (e) => { if (e.target === div) { div.classList.remove('active'); div.remove(); } }); return id; } }; // ============================================================ // Utility Functions // ============================================================ function formatDate(dateStr) { if (!dateStr) return '-'; const d = new Date(dateStr); return d.toLocaleDateString('th-TH', { year: 'numeric', month: 'short', day: 'numeric' }); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function debounce(fn, delay = 300) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } // ============================================================ // Sidebar Toggle (Mobile) // ============================================================ function toggleSidebar() { const sidebar = document.querySelector('.sidebar'); const overlay = document.querySelector('.sidebar-overlay'); if (sidebar) sidebar.classList.toggle('open'); if (overlay) overlay.classList.toggle('active'); } // Close sidebar when clicking overlay document.addEventListener('click', (e) => { if (e.target.classList.contains('sidebar-overlay')) { toggleSidebar(); } }); // Init Theme.init();