/** * TPU Survey - Core Application Logic * API communication, Auth state, Toast notifications, Theme */ const getAppBasePath = () => { return window.location.pathname.includes('/survey') ? '/survey' : ''; }; 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; }); } }; // ============================================================ // 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 = `